From 7124c49d7d15b568ba597eb8e4725e1e635671f5 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 3 Apr 2024 08:51:35 -0300 Subject: [PATCH 001/221] Add Web UI loading state pixels (#2531) --- DuckDuckGo/DBP/DBPHomeViewController.swift | 5 +- .../Pixels/DataBrokerProtectionPixels.swift | 21 ++- .../DataBrokerProtectionWebUIPixels.swift | 69 +++++++ .../DataBrokerProtectionViewController.swift | 36 ++++ ...DataBrokerProtectionWebUIPixelsTests.swift | 168 ++++++++++++++++++ 5 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionWebUIPixels.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionWebUIPixelsTests.swift diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index f3b2cf95d0..fdfca10e97 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -195,7 +195,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping + private var wasHTTPErrorPixelFired = false + + init(pixelHandler: EventMapping) { + self.pixelHandler = pixelHandler + } + + func firePixel(for error: Error) { + if wasHTTPErrorPixelFired { + wasHTTPErrorPixelFired = false // We reset the flag + return + } + + let nsError = error as NSError + + if nsError.domain == NSURLErrorDomain { + let statusCode = nsError.code + if statusCode >= 400 && statusCode < 600 { + pixelHandler.fire(.webUILoadingFailed(errorCategory: "httpError-\(statusCode)")) + wasHTTPErrorPixelFired = true + } else { + pixelHandler.fire(.webUILoadingFailed(errorCategory: "other-\(nsError.code)")) + } + } else { + pixelHandler.fire(.webUILoadingFailed(errorCategory: "other-\(nsError.code)")) + } + } + + func firePixel(for selectedURL: DataBrokerProtectionWebUIURLType, type: PixelType) { + let environment = selectedURL == .custom ? "staging" : "production" + + switch type { + case .loading: + pixelHandler.fire(.webUILoadingStarted(environment: environment)) + case .success: + pixelHandler.fire(.webUILoadingSuccess(environment: environment)) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index 9ac49e395a..688de0476e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -18,7 +18,9 @@ import Cocoa import SwiftUI +import Common import BrowserServicesKit +import PixelKit import WebKit import Combine @@ -29,6 +31,8 @@ final public class DataBrokerProtectionViewController: NSViewController { private var loader: NSProgressIndicator! private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable private let webUIViewModel: DBPUIViewModel + private let pixelHandler: EventMapping + private let webUIPixel: DataBrokerProtectionWebUIPixels private let openURLHandler: (URL?) -> Void private var reloadObserver: NSObjectProtocol? @@ -43,6 +47,8 @@ final public class DataBrokerProtectionViewController: NSViewController { self.dataManager = dataManager self.openURLHandler = openURLHandler self.webUISettings = webUISettings + self.pixelHandler = DataBrokerProtectionPixelsHandler() + self.webUIPixel = DataBrokerProtectionWebUIPixels(pixelHandler: pixelHandler) self.webUIViewModel = DBPUIViewModel(dataManager: dataManager, scheduler: scheduler, webUISettings: webUISettings, @@ -82,6 +88,7 @@ final public class DataBrokerProtectionViewController: NSViewController { addLoadingIndicator() if let url = URL(string: webUISettings.selectedURL) { + webUIPixel.firePixel(for: webUISettings.selectedURLType, type: .loading) webView?.load(url) } else { removeLoadingIndicator() @@ -126,11 +133,40 @@ extension DataBrokerProtectionViewController: WKUIDelegate { extension DataBrokerProtectionViewController: WKNavigationDelegate { + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) { + fireWebViewError(error) + } + + public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) { + fireWebViewError(error) + } + + private func fireWebViewError(_ error: Error) { + webUIPixel.firePixel(for: error) + removeLoadingIndicator() + } + + public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { + guard let statusCode = (navigationResponse.response as? HTTPURLResponse)?.statusCode else { + // if there's no http status code to act on, exit and allow navigation + return .allow + } + + if statusCode >= 400 { + webUIPixel.firePixel(for: NSError(domain: NSURLErrorDomain, code: statusCode)) + return .cancel + } + + return .allow + } + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { loader.startAnimation(nil) } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { removeLoadingIndicator() + + webUIPixel.firePixel(for: webUISettings.selectedURLType, type: .success) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionWebUIPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionWebUIPixelsTests.swift new file mode 100644 index 0000000000..f42517fb5a --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionWebUIPixelsTests.swift @@ -0,0 +1,168 @@ +// +// DataBrokerProtectionWebUIPixelsTests.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 XCTest +import Foundation +@testable import DataBrokerProtection + +final class DataBrokerProtectionWebUIPixelsTests: XCTestCase { + + let handler = MockDataBrokerProtectionPixelsHandler() + + override func tearDown() { + handler.clear() + } + + func testWhenURLErrorIsHttp_thenCorrectPixelIsFired() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 404)) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.params!["error_category"], + "httpError-404" + ) + } + + func testWhenURLErrorIsNotHttp_thenCorrectPixelIsFired() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 100)) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.params!["error_category"], + "other-100" + ) + } + + func testWhenErrorIsNotURL_thenCorrectPixelIsFired() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500)) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.params!["error_category"], + "other-500" + ) + } + + func testWhenSelectedURLisCustomAndLoading_thenStagingParamIsSent() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: .custom, type: .loading) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.name, + DataBrokerProtectionPixels.webUILoadingStarted(environment: "staging").name + ) + XCTAssertEqual( + lastPixelFired.params!["environment"], + "staging" + ) + } + + func testWhenSelectedURLisProductionAndLoading_thenProductionParamIsSent() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: .production, type: .loading) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.name, + DataBrokerProtectionPixels.webUILoadingStarted(environment: "staging").name + ) + XCTAssertEqual( + lastPixelFired.params!["environment"], + "production" + ) + } + + func testWhenSelectedURLisCustomAndSuccess_thenStagingParamIsSent() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: .custom, type: .success) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.name, + DataBrokerProtectionPixels.webUILoadingSuccess(environment: "staging").name + ) + XCTAssertEqual( + lastPixelFired.params!["environment"], + "staging" + ) + } + + func testWhenSelectedURLisProductionAndSuccess_thenProductionParamIsSent() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: .production, type: .success) + + let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + lastPixelFired.name, + DataBrokerProtectionPixels.webUILoadingSuccess(environment: "staging").name + ) + XCTAssertEqual( + lastPixelFired.params!["environment"], + "production" + ) + } + + func testWhenHTTPPixelIsFired_weDoNotFireAnotherPixelRightAway() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 404)) + sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500)) + + let httpPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + httpPixel.params!["error_category"], + "httpError-404" + ) + XCTAssertEqual(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count, 1) // We only fire one pixel + } + + func testWhenHTTPPixelIsFired_weFireTheNextErrorPixelOnTheSecondTry() { + let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler) + + sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 404)) + sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500)) + sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500)) + + let httpPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first! + + XCTAssertEqual( + httpPixel.params!["error_category"], + "httpError-404" + ) + XCTAssertEqual(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count, 2) // We fire the HTTP pixel and the second cocoa error pixel + } +} From b484b1e410894d344d6ef02a224f592e166630a3 Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:35:08 +0200 Subject: [PATCH 002/221] Closing empty tabs after download (#2510) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206835038894671/f **Description**: Fix of empty tabs remaining in the tab bar after a download finishes. --- .../TabExtensions/DownloadsTabExtension.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 65eb64e98a..2da642e537 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -213,16 +213,23 @@ extension DownloadsTabExtension: NavigationResponder { func enqueueDownload(_ download: WebKitDownload, withNavigationAction navigationAction: NavigationAction?) { let task = downloadManager.add(download, fromBurnerWindow: self.isBurner, delegate: self, destination: .auto) + var isMainFrameNavigationActionWithNoHistory: Bool { + guard let navigationAction, + navigationAction.isForMainFrame, + navigationAction.isTargetingNewWindow, + // webView has no navigation history (downloaded navigationAction has started from an empty state) + (navigationAction.redirectHistory?.first ?? navigationAction).fromHistoryItemIdentity == nil + else { return false } + return true + } + // If the download has started from a popup Tab - close it after starting the download // e.g. download button on this page: // https://en.wikipedia.org/wiki/Guitar#/media/File:GuitareClassique5.png - guard let navigationAction, - navigationAction.isForMainFrame, - navigationAction.isTargetingNewWindow, - let webView = download.webView, - // webView has no navigation history (downloaded navigationAction has started from an empty state) - (navigationAction.redirectHistory?.first ?? navigationAction).fromHistoryItemIdentity == nil - else { return } + guard let webView = download.webView, + isMainFrameNavigationActionWithNoHistory + // if converted from navigation response but no page was loaded + || navigationAction == nil && webView.backForwardList.currentItem == nil else { return } self.closeWebView(webView, afterDownloadTaskHasStarted: task) } From 3b42790cb73c177fed42937921ded642af326f01 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 3 Apr 2024 22:45:25 +0200 Subject: [PATCH 003/221] Redirect from purchase page to macOS native purchase flow (#2538) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206989054564349/f **Description**: Redirect from purchase page to macOS native purchase flow **Steps to test this PR**: 1.Navigation should be redirected to the subscription purchase page - [x] Open https://www.duckduckgo.com/pro - [x] Open https://duckduckgo.com/pro - [x] Open https://sbarag.duckduckgo.com/pro Note: Try navigating via different means (e.g. type in the URL, open a link, bookmark etc.): 2. No navigation triggered on not matching URLs - [x] Open https://www.duckduckgo.com/pros - [x] Open https://www.duckduckgoo.com/pro 3. Disabled redirect when feature is not available. _Ensure feature flag is disabled and disable internal user state._ - [x] Open https://www.duckduckgo.com/pro 4. Disabled redirect when on App Store and no products are available _Predefine/mock `SubscriptionPurchaseEnvironment.current == .appStore` and `SubscriptionPurchaseEnvironment.canPurchase == false` to simulate App Store build with no available products_ - [x] Open https://www.duckduckgo.com/pro --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 9 +++- .../Common/Extensions/URLExtension.swift | 4 ++ DuckDuckGo/Tab/Model/Tab+Navigation.swift | 2 + .../RedirectNavigationResponder.swift | 48 +++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2c93efafdd..78225c14cb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -158,6 +158,9 @@ 1E2AE4C82ACB216B00684E0A /* HoverTrackingArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B140872ABDBCC1004F8E85 /* HoverTrackingArea.swift */; }; 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF15D82ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift */; }; 1E2AE4CB2ACB21C800684E0A /* HardwareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9579202AC687170062CA31 /* HardwareModel.swift */; }; + 1E559BB12BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; + 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; + 1E559BB32BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */; }; 1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */; }; 1E7E2E942902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */; }; 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E3E2912A10D0051A99B /* ContentBlocking */; }; @@ -3586,6 +3589,7 @@ 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtension.swift; sourceTree = ""; }; 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtensionTests.swift; sourceTree = ""; }; 1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = ""; }; + 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedirectNavigationResponder.swift; sourceTree = ""; }; 1E7E2E8F29029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockingRulesUpdateObserver.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 1E862A882A9FC01200F84D4B /* SubscriptionUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SubscriptionUI; sourceTree = ""; }; @@ -8554,6 +8558,7 @@ isa = PBXGroup; children = ( B6BF5D922947199A006742B1 /* SerpHeadersNavigationResponder.swift */, + 1E559BB02BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift */, B60C6F7629B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift */, B687B7CB2947A1E9001DEA6F /* ExternalAppSchemeHandler.swift */, ); @@ -10502,6 +10507,7 @@ 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, + 1E559BB22BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 7B7FCD102BA33B2700C04FBE /* UserDefaults+vpnLegacyUser.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, @@ -11935,6 +11941,7 @@ 4B957AFC2AC7AE700062CA31 /* SyncCredentialsAdapter.swift in Sources */, 4B957AFD2AC7AE700062CA31 /* WKUserContentControllerExtension.swift in Sources */, 4B957AFE2AC7AE700062CA31 /* EditableTextView.swift in Sources */, + 1E559BB32BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 4B957AFF2AC7AE700062CA31 /* TabCollection.swift in Sources */, 4B957B002AC7AE700062CA31 /* MainView.swift in Sources */, 4B957B012AC7AE700062CA31 /* Tab+Navigation.swift in Sources */, @@ -12890,6 +12897,7 @@ 85F0FF1327CFAB04001C7C6E /* RecentlyVisitedView.swift in Sources */, AA7EB6DF27E7C57D00036718 /* MouseOverAnimationButton.swift in Sources */, AA7412B724D1687000D22FE0 /* TabBarScrollView.swift in Sources */, + 1E559BB12BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, 4B9292D92667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift in Sources */, 14D9B8FB24F7E089000D4D13 /* AddressBarViewController.swift in Sources */, B65536A62685B82B00085A79 /* Permissions.swift in Sources */, @@ -14476,7 +14484,6 @@ }; 3706FA6B293F65D500E42796 /* TrackerRadarKit */ = { isa = XCSwiftPackageProductDependency; - package = 3706FA6C293F65D500E42796 /* XCRemoteSwiftPackageReference "TrackerRadarKit" */; productName = TrackerRadarKit; }; 3706FA71293F65D500E42796 /* BrowserServicesKit */ = { diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 4c68d32b01..6ea129d2fb 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -373,6 +373,10 @@ extension URL { return URL(string: "https://duckduckgo.com/privacy")! } + static var privacyPro: URL { + return URL(string: "https://duckduckgo.com/pro")! + } + static var duckDuckGoEmail = URL(string: "https://duckduckgo.com/email-protection")! static var duckDuckGoEmailLogin = URL(string: "https://duckduckgo.com/email")! diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index 7addf60028..6ada06456b 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -70,6 +70,8 @@ extension Tab: NavigationResponder { // add extra headers to SERP requests .struct(SerpHeadersNavigationResponder()), + .struct(RedirectNavigationResponder()), + // ensure Content Blocking Rules are applied before navigation .weak(nullable: self.contentBlockingAndSurrogates), // update click-to-load state diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift new file mode 100644 index 0000000000..af59446c0e --- /dev/null +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -0,0 +1,48 @@ +// +// RedirectNavigationResponder.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 Navigation +import Foundation +import Subscription + +struct RedirectNavigationResponder: NavigationResponder { + + func decidePolicy(for navigationAction: NavigationAction, preferences: inout NavigationPreferences) async -> NavigationActionPolicy? { + guard let mainFrame = navigationAction.mainFrameTarget, let redirectURL = redirectURL(for: navigationAction.url) else { return .next } + + return .redirect(mainFrame) { navigator in + var request = navigationAction.request + request.url = redirectURL + navigator.load(request) + } + } + + private func redirectURL(for url: URL) -> URL? { + guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } + + if url.pathComponents == URL.privacyPro.pathComponents { + let isFeatureAvailable = DefaultSubscriptionFeatureAvailability().isFeatureAvailable + let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts + + return isPurchasePageRedirectActive ? URL.subscriptionPurchase : nil + } + + return nil + } +} From 5a46ee61be013dd690c4058b076e3b7139e18cfd Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 3 Apr 2024 17:19:28 -0700 Subject: [PATCH 004/221] Use the default action button style for VPN onboarding (#2529) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206958823796186/f Tech Design URL: CC: Description: This PR changes the onboarding button style to the default action style, to fix UX problems where the button doesn't appear clickable enough. --- .../PromptActionView/PromptActionView.swift | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift index 8304b3ac0b..ab497e050b 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/PromptActionView/PromptActionView.swift @@ -30,35 +30,6 @@ fileprivate extension View { .foregroundColor(Color(.defaultText)) } - @ViewBuilder - func applyStepButtonAttributes(colorScheme: ColorScheme) -> some View { - switch colorScheme { - case .dark: - self.buttonStyle(.plain) - .padding(.horizontal, 12) - .padding(.vertical, 0) - .frame(height: 20, alignment: .center) - .background(Color(.onboardingButtonBackgroundColor)) - .cornerRadius(5) - .shadow(color: .black.opacity(0.2), radius: 0.5, x: 0, y: 1) - .shadow(color: .black.opacity(0.05), radius: 0.5, x: 0, y: 0) - .shadow(color: .black.opacity(0.1), radius: 0, x: 0, y: 0) - default: - self.buttonStyle(.plain) - .padding(.horizontal, 12) - .padding(.vertical, 0) - .frame(height: 20, alignment: .center) - .background(Color(.onboardingButtonBackgroundColor)) - .cornerRadius(5) - .shadow(color: .black.opacity(0.1), radius: 0.5, x: 0, y: 1) - .shadow(color: .black.opacity(0.05), radius: 0.5, x: 0, y: 0) - .overlay( - RoundedRectangle(cornerRadius: 5) - .inset(by: -0.25) - .stroke(.black.opacity(0.1), lineWidth: 0.5) - ) - } - } } struct PromptActionView: View { @@ -95,7 +66,7 @@ struct PromptActionView: View { .multilineText() Button(model.actionTitle, action: model.action) - .applyStepButtonAttributes(colorScheme: colorScheme) + .keyboardShortcut(.defaultAction) .padding(.top, 3) } From f30373f7fc19d687e87667c50f6d815968267d96 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 4 Apr 2024 05:12:26 +0000 Subject: [PATCH 005/221] Bump version to 1.82.0 (151) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index a22b48d2cf..f2874c8de9 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 150 +CURRENT_PROJECT_VERSION = 151 From 26dfe3f172902e6b1ef4d6237ea4e91f348b6a30 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Thu, 4 Apr 2024 10:54:17 +0100 Subject: [PATCH 006/221] Improve Handling of noData Import Errors (#2494) Task/Issue URL: https://app.asana.com/0/0/1206886138223455/f **Description**: Stop prompting the user for feedback when we are handling a noData error. When no data is found initially, and user chooses to skip manual import, no summary screen is displayed (the import dialog is dismissed) --- .../Images/Error.imageset/Contents.json | 2 +- ...ror.pdf => Exclamation-Recolorable-16.pdf} | Bin 1801 -> 1697 bytes .../Images/Skipped.imageset/Contents.json | 12 ++ .../Skipped.imageset/Jump-Recolorable-16.pdf | Bin 0 -> 2069 bytes .../Check-Recolorable-16.pdf | Bin 0 -> 1560 bytes .../SuccessCheckmark.imageset/Contents.json | 2 +- .../SuccessCheckmark.pdf | Bin 1507 -> 0 bytes DuckDuckGo/Common/Localizables/UserText.swift | 7 + .../Firefox/FirefoxBookmarksReader.swift | 8 +- .../Logins/Firefox/FirefoxLoginReader.swift | 8 +- .../Model/DataImportViewModel.swift | 19 ++- .../View/DataImportNoDataView.swift | 6 +- .../View/DataImportSummaryView.swift | 38 +++++- DuckDuckGo/Localizable.xcstrings | 126 +++++++++++++++++- .../DataImport/DataImportViewModelTests.swift | 102 ++++++++++---- .../FirefoxBookmarksReaderTests.swift | 17 +++ .../DataImport/FirefoxLoginReaderTests.swift | 22 ++- 17 files changed, 322 insertions(+), 47 deletions(-) rename DuckDuckGo/Assets.xcassets/Images/Error.imageset/{Error.pdf => Exclamation-Recolorable-16.pdf} (50%) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Jump-Recolorable-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Check-Recolorable-16.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/SuccessCheckmark.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/Error.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Error.imageset/Contents.json index c8ff07440c..98276ca193 100644 --- a/DuckDuckGo/Assets.xcassets/Images/Error.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/Error.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Error.pdf", + "filename" : "Exclamation-Recolorable-16.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/Error.imageset/Error.pdf b/DuckDuckGo/Assets.xcassets/Images/Error.imageset/Exclamation-Recolorable-16.pdf similarity index 50% rename from DuckDuckGo/Assets.xcassets/Images/Error.imageset/Error.pdf rename to DuckDuckGo/Assets.xcassets/Images/Error.imageset/Exclamation-Recolorable-16.pdf index f8efeca609c4e025751136c335010f42a7e3f66c..4f4f2518c48d40f216f48ff3983c2fe1073b1e57 100644 GIT binary patch literal 1697 zcma)7O>fjN5WV|X_)@7ws^hQtL#is#Eky_rWy`JN5VCH&Xg7gPiVDA;nQSsnw;b?c z4nRy=19$jBvohsoOQ(W@)+jqwK*%?1SXQ^4?C)0Dh_>h|W{(&0+muS`XyHssi zahZOt>$H4(!5264uexJDnG$kXTuz%)+PyrZqk7Avh#Fnd)2U!1Y?OD}Mc&m73lkst zY?;!+s-S(y(UhC)j3|ThTJsDMN6j*rNoG4@A(!k>vwu)iR+)q#;;5m>Q68e84IPm= zAvJqocduB(KL4-6P6S_~m}UwH3X}dXH>j`_UY1fR@Ub9VR2(k_5aUe7q%g)t;{hps z3>w-45O8yV_+YFu9P3ngo0>=Alv7}}K`eNwJYYdPXB~Q$&M3PN2w7%lEu@V)#2f(;YoyJ2AX|h6i>S?*FOo&9Gv=z`p+KE>R=EqrUtui4D!%;wk$7=k-KP=wm{#lRqJ84G`MFa9`FZBu#I#IZ`?#x9$8qp2N4iDZ z^r?Y}IQ1s*y4nH8%MqH*Mucc)7Vp<9NR?v+OFqR}w2$DcUgX~aGGl_ipD|&99R7x9SkUIV zm4$acLH({;Z_dkq5WV|X%w?sdQVa%cgA_$-cAKhdt8PheQ4f@LHdMRN0;$sc`i{Yj*^*WB8_zFvk5i%#ayOBM-h04I7;S0PcpNJg+hEl$zGtQsyqon#8G0CZahRm23?Um zAtifY_peyZKI4Q#u0t(C4LDe5Ee*U=I>jF-9wk{8C4eKQ1uP9aV#=p^oQgD7V7KB5 z)?8;Qlp&5fj-^f&q+G;<7p9li%OfSrc=Q)`KuoD560!FaOZqwFs=#fLpW>O)3b#sR zimR~4;{^u?jsyHMM7_fip0t+{6*Xf}TQ!^f?pj3prKjzB3GE>Jk!iLuTT zNT3KTAdPD}^-2<~^%Rnk5A55bI+&K(?Y-OJIL^A}XwKoR+MSO>3G$}t!8SY$IJm8z W4OTz=X%%fhv?HX>*lP9W)6E}+`gn-| diff --git a/DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Contents.json new file mode 100644 index 0000000000..89a905405a --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Jump-Recolorable-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Jump-Recolorable-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Skipped.imageset/Jump-Recolorable-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f0c7598cc801ef4ffc5c628f626836798ccbd1fb GIT binary patch literal 2069 zcma)7O^*{f5WV|X_%af#gyXVZ{*VyTG6M*~YKIIrw1;d@Gh~(NX43%yzn)i}j=N`u z16CBdpX>8gxqNnc@$w0oq!ekZ`+xsaT0ecNpFLA;cP*dFE%DXcw!1le(jMSiq=v)3 z-Q23xMf-KzwCh*T_4(EEs~Ob)ii{nXwx`XLx_fw5kD6O$4SQM8yuTTmPUVxms&lKz znLyd4JbR;flJkZ$CFk@ERU3_(!P&@SSFKc)S)ozCFkPdXjZ~=GXprPYPc>0D^{9$h zq*0&L$3Il3-u*7a&cx__AstL{!Bf;=xbpvSBMg%@-bcwNqIw=qqlm8;zQAs*I+9#QPvsN zm2iNR;5iWx7ej`>U|cFeQdSB5ky7x;h?so{2*wpAD@Y2(=kZ8#wLl8nz#e)qN;$BD zI5BZx?I6z36sf?eBuysU%svD0iHZ%-vnD69HX;Uy6ra$T&O4|g&Rjw%MOu}-6G+QKDAcTjc!=bXS1^b-yCX5Ea{{+W&_>4Xf_RGJ zCLni@pKMBYXa>j-Xb@u9WQPnbFph}DgIV|hH9*9jwK_?|IMm3aYBoSv3RsXGIt=`S zKTasKkddS!7EOwZWD}5|lSI86+fn?LI5uR(cEBQy4OuZ%?%43S5Mk`?idcjFs~mwz2GOjrY+ z7Fj*+bXOsdIAv7`g9Pu?Jt%;kyHKJBiYd>Ehy7-|Yxhd3_xb{KJR17j_OX7ny?%To kWwq}6gO-Fxf>&4DA1$IEAMnloFs6gymekqVi@#s~1XXse`Tzg` literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Check-Recolorable-16.pdf b/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Check-Recolorable-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..719cb643de9f549679735432b0e111415fa25614 GIT binary patch literal 1560 zcma)+O>Yx15Qgvm6?3W7BGo(NZ%b7r+ERo7QBrOdhh^ioLCG#;x2W*z8E+GB+Hk;! z$a(B}J@b0U2Ui!D$0WTF1PI-??*icD1Wr#yYUlZ{P`kYPklMT99yG&Q-qjE5RIfyJ zk$x?jG<$mn=hwUcnqK@AByk*o+sSd6?XQ)kW+hOPiiS7`>201(5`EK(u+=LuD?v&c z3d)CRE4l4HEo~sL6ildSG-862$j+>&l#nT)5&w`bSxq7Z6^(|A+}Kkj%A_on?S(Yr zUfjMCt@!-E4jVSp@=;)9KqODV4j2FTC)r^yts(C$SQ*LM7-7ZC$B5{dxnPZU;AK!4 zt!JVlYVu5ML?3e;YK`7Oc1W9%p;TdHOJ<59QobxQE_w}GqR|2Km7~XK6et-SDHhGd zL~mJ3N#}#pnaH~=x@b&Ra>^qy(UCtN%S59=6FDv)!_4Jl?8ufPe~jL%U>Ise z3N>V;SvN59WTMN5D;1ToOrqA-(gu7?^F@8uJ;DuF{Ia>H zu9#+9rCI8`hjo*B$nVW;BX{1lLu&cN{Mc;aRlQ~ge0?L@d6F_JPgV7PG3QhvS1^O^ zIp^I2$I@Q@J(C?JzOWsoTwq@goC-g$hkDuFJr8|cr(1#G#Z&qD7o4@)0^`jfgmDIK zL7MZ-qaeb)U4)vAc)m*^SN#I{SkSPp7t6F3{B~FGF4&I4zFVauI9$w+o=mA`T{l23 ccr>`WUi?X%{rHC0>tQU1v<~9n;LWGYzh{9;VgLXD literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Contents.json index 09a818b90b..07d8227e97 100644 --- a/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "SuccessCheckmark.pdf", + "filename" : "Check-Recolorable-16.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/SuccessCheckmark.pdf b/DuckDuckGo/Assets.xcassets/Images/SuccessCheckmark.imageset/SuccessCheckmark.pdf deleted file mode 100644 index bb50d381fa0c14409d842a68bcf87bdd05ab8ac1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1507 zcma)6O>fjN5WV|X%%xI`RL5TvJ5p7NZYe^5Shn0M4k7Edi*^%8QdIiu8OMo}QV#gA za^8C0ym{mC(fac0RC3Fh1Od&r?+oDV49?G)ueafosU@C$@bz7P51PPDIMwy7FAprc z^uKnM-@Ltm)%EhL>ex>vxfm9RX>*!(i~sUGM`7p?ZHhucbzp)^RTxPcxW%xWDY~j= z(uA{COR!vPEwzKg0@PeNE$CkL-gpMV~ zRLUSEuBbAUHUxy6GgPrz+>FkJ&3rSq$a~LjSM7CrrAcKoMGt z&I$*LJ1GPX&7KgnXo)n%1g_Bo#sh8<_H} zng@!NFZ6eWESTtSESO@MtFkZm&D|Ipithd;;<5F&On_x^hQlx9h;*w79+#y185YzD z(xme_RG@tU(OrCgN=hVeo&`#!Nd5@*ZMoa~meDhuy}Kkkj=SdIPvCgBJ&C4do2Ka@ d6g(K5UGJVesUM!>vh9a*q;klPj^2E_`U8SYL7@Nu diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 5f17e7d9b2..e78ee81e96 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -636,6 +636,13 @@ struct UserText { String(format: NSLocalizedString("import.logins.select-csv-file.source", value: "Select %@ CSV File…", comment: "Button text for selecting a CSV file exported from (LastPass or Bitwarden or 1Password - %@)"), source.importSourceName) } + static func importNoDataBookmarksSubtitle(from source: DataImport.Source) -> String { + String(format: NSLocalizedString("import.nodata.bookmarks.subtitle", value: "If you have %@ bookmarks, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file."), source.importSourceName) + } + static func importNoDataPasswordsSubtitle(from source: DataImport.Source) -> String { + String(format: NSLocalizedString("import.nodata.passwords.subtitle", value: "If you have %@ passwords, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import passwords manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox"), source.importSourceName) + } + static let importLoginsPasswords = NSLocalizedString("import.logins.passwords", value: "Passwords", comment: "Title text for the Passwords import option") static let importBookmarksButtonTitle = NSLocalizedString("bookmarks.import.button.title", value: "Import", comment: "Button text to open bookmark import dialog") diff --git a/DuckDuckGo/DataImport/Bookmarks/Firefox/FirefoxBookmarksReader.swift b/DuckDuckGo/DataImport/Bookmarks/Firefox/FirefoxBookmarksReader.swift index fdf0029709..afef50085b 100644 --- a/DuckDuckGo/DataImport/Bookmarks/Firefox/FirefoxBookmarksReader.swift +++ b/DuckDuckGo/DataImport/Bookmarks/Firefox/FirefoxBookmarksReader.swift @@ -45,6 +45,8 @@ final class FirefoxBookmarksReader { case fetchAllFolders case copyTemporaryFile + + case couldNotFindBookmarksFile } var action: DataImportAction { .bookmarks } @@ -53,7 +55,7 @@ final class FirefoxBookmarksReader { var errorType: DataImport.ErrorType { switch type { - case .dbOpen: .noData + case .dbOpen, .couldNotFindBookmarksFile: .noData case .fetchRootEntries, .noRootEntries, .fetchTopLevelFolders, .fetchAllBookmarks, .fetchAllFolders: .dataCorrupted case .copyTemporaryFile: .other } @@ -72,6 +74,10 @@ final class FirefoxBookmarksReader { } func readBookmarks() -> DataImportResult { + guard FileManager.default.fileExists(atPath: firefoxPlacesDatabaseURL.path) else { + return .failure(ImportError(type: .couldNotFindBookmarksFile, underlyingError: nil)) + } + do { currentOperationType = .copyTemporaryFile return try firefoxPlacesDatabaseURL.withTemporaryFile { temporaryDatabaseURL in diff --git a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxLoginReader.swift b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxLoginReader.swift index 3c354886cc..af702efd16 100644 --- a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxLoginReader.swift +++ b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxLoginReader.swift @@ -41,6 +41,8 @@ final class FirefoxLoginReader { case decryptUsername case decryptPassword + + case couldNotFindKeyDB } var action: DataImportAction { .passwords } @@ -49,7 +51,7 @@ final class FirefoxLoginReader { var errorType: DataImport.ErrorType { switch type { - case .couldNotFindLoginsFile, .couldNotReadLoginsFile: .noData + case .couldNotFindLoginsFile, .couldNotFindKeyDB, .couldNotReadLoginsFile: .noData case .key3readerStage1, .key3readerStage2, .key3readerStage3, .key4readerStage1, .key4readerStage2, .key4readerStage3, .decryptUsername, .decryptPassword: .decryptionError case .couldNotDetermineFormat: .dataCorrupted case .requiresPrimaryPassword: .other @@ -94,7 +96,7 @@ final class FirefoxLoginReader { func readLogins(dataFormat: DataFormat?) -> DataImportResult<[ImportedLoginCredential]> { var currentOperationType: ImportError.OperationType = .couldNotFindLoginsFile do { - let dataFormat = try dataFormat ?? detectLoginFormat() ?? { throw ImportError(type: .couldNotDetermineFormat, underlyingError: nil) }() + let dataFormat = try dataFormat ?? detectLoginFormat() ?? { throw ImportError(type: .couldNotFindKeyDB, underlyingError: nil) }() let keyData = try getEncryptionKey(dataFormat: dataFormat) let result = try reallyReadLogins(dataFormat: dataFormat, keyData: keyData, currentOperationType: ¤tOperationType) return .success(result) @@ -106,7 +108,7 @@ final class FirefoxLoginReader { } func getEncryptionKey() throws -> Data { - let dataFormat = try detectLoginFormat() ?? { throw ImportError(type: .couldNotDetermineFormat, underlyingError: nil) }() + let dataFormat = try detectLoginFormat() ?? { throw ImportError(type: .couldNotFindKeyDB, underlyingError: nil) }() return try getEncryptionKey(dataFormat: dataFormat) } diff --git a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift index 1a2d1eef7b..8757194031 100644 --- a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift @@ -107,6 +107,11 @@ struct DataImportViewModel { self.dataType = dataType self.result = result } + + static func == (lhs: DataTypeImportResult, rhs: DataTypeImportResult) -> Bool { + lhs.dataType == rhs.dataType && + lhs.result.description == rhs.result.description + } } /// collected import summary for current import operation per selected import source @@ -248,6 +253,7 @@ struct DataImportViewModel { // switch to file import of the failed data type displaying successful import results nextScreen = .fileImport(dataType: dataType, summary: Set(summary.filter({ $0.value.isSuccess }).keys)) } + Pixel.fire(.dataImportFailed(source: importSource, sourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile), error: error)) } } @@ -322,16 +328,19 @@ struct DataImportViewModel { } /// Skip button press - @MainActor mutating func skipImport() { + @MainActor mutating func skipImportOrDismiss(using dismiss: @escaping () -> Void) { if let screen = screenForNextDataTypeRemainingToImport(after: screen.fileImportDataType) { // skip to next non-imported data type self.screen = screen - } else if selectedDataTypes.first(where: { error(for: $0) != nil }) != nil { + } else if selectedDataTypes.first(where: { + let error = error(for: $0) + return error != nil && error?.errorType != .noData + }) != nil { // errors occurred during import: show feedback screen self.screen = .feedback } else { - // display total summary - self.screen = .summary(selectedDataTypes) + // When we skip a manual import, and there are no next non-imported data types, we dismiss + self.dismiss(using: dismiss) } } @@ -671,7 +680,7 @@ extension DataImportViewModel { initiateImport() case .skip: - skipImport() + skipImportOrDismiss(using: dismiss) case .cancel: importTask?.cancel() diff --git a/DuckDuckGo/DataImport/View/DataImportNoDataView.swift b/DuckDuckGo/DataImport/View/DataImportNoDataView.swift index 84fb50a583..889daccbab 100644 --- a/DuckDuckGo/DataImport/View/DataImportNoDataView.swift +++ b/DuckDuckGo/DataImport/View/DataImportNoDataView.swift @@ -31,15 +31,13 @@ struct DataImportNoDataView: View { Text("We couldn‘t find any bookmarks.", comment: "Data import error message: Bookmarks weren‘t found.") .bold() - Text("Try importing bookmarks manually instead.", - comment: "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file.") + Text(UserText.importNoDataBookmarksSubtitle(from: source)) case .passwords: Text("We couldn‘t find any passwords.", comment: "Data import error message: Passwords weren‘t found.") .bold() - Text("Try importing passwords manually instead.", - comment: "Data import error subtitle: suggestion to import Passwords manually by selecting a CSV or HTML file.") + Text(UserText.importNoDataPasswordsSubtitle(from: source)) } } } diff --git a/DuckDuckGo/DataImport/View/DataImportSummaryView.swift b/DuckDuckGo/DataImport/View/DataImportSummaryView.swift index f9eddeecfb..b385a90c66 100644 --- a/DuckDuckGo/DataImport/View/DataImportSummaryView.swift +++ b/DuckDuckGo/DataImport/View/DataImportSummaryView.swift @@ -33,6 +33,8 @@ struct DataImportSummaryView: View { self.model = model } + private let zeroString = "0" + var body: some View { VStack(alignment: .leading, spacing: 8) { { @@ -61,7 +63,7 @@ struct DataImportSummaryView: View { } if summary.duplicate > 0 { HStack { - failureImage() + skippedImage() Text("Duplicate Bookmarks Skipped:", comment: "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.") + Text(" " as String) @@ -78,6 +80,15 @@ struct DataImportSummaryView: View { } } + case (.bookmarks, .failure(let error)) where error.errorType == .noData: + HStack { + skippedImage() + Text("Bookmarks:", + comment: "Data import summary format of how many bookmarks were successfully imported.") + + Text(" " as String) + + Text(zeroString).bold() + } + case (.bookmarks, .failure): HStack { failureImage() @@ -85,11 +96,21 @@ struct DataImportSummaryView: View { comment: "Data import summary message of failed bookmarks import.") } - case (.passwords, .failure): - HStack { - failureImage() - Text("Password import failed.", - comment: "Data import summary message of failed passwords import.") + case (.passwords, .failure(let error)): + if error.errorType == .noData { + HStack { + skippedImage() + Text("Passwords:", + comment: "Data import summary format of how many passwords were successfully imported.") + + Text(" " as String) + + Text(zeroString).bold() + } + } else { + HStack { + failureImage() + Text("Password import failed.", + comment: "Data import summary message of failed passwords import.") + } } case (.passwords, .success(let summary)): @@ -126,6 +147,11 @@ private func failureImage() -> some View { .frame(width: 16, height: 16) } +private func skippedImage() -> some View { + Image(.skipped) + .frame(width: 16, height: 16) +} + #if DEBUG #Preview { VStack { diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index bcd4fd7fac..288c5a5bc5 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -24263,6 +24263,126 @@ } } }, + "import.nodata.bookmarks.subtitle" : { + "comment" : "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn du %@ Lesezeichen hast, versuche stattdessen, sie manuell zu importieren." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "If you have %@ bookmarks, try importing them manually instead." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si tienes %@ marcadores, intenta importarlos manualmente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si vous avez %@ signets, essayez plutôt de les importer manuellement." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se hai %@ segnalibri, prova a importarli manualmente." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Als je %@ bladwijzers hebt, probeer ze dan handmatig te importeren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jeśli masz zakładki %@, spróbuj zaimportować je ręcznie." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se tens favoritos do %@, tenta importá-los manualmente." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Если у вас есть закладки в %@, попробуйте импортировать их вручную." + } + } + } + }, + "import.nodata.passwords.subtitle" : { + "comment" : "Data import error subtitle: suggestion to import passwords manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wenn du %@ Passwörter hast, versuche stattdessen, sie manuell zu importieren." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "If you have %@ passwords, try importing them manually instead." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si tiene %@ contraseñas, intenta importarlas manualmente." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si vous avez %@ mots de passe, essayez plutôt de les importer manuellement." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se hai %@ password, prova a importarle manualmente." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Als je %@ wachtwoorden hebt, probeer ze dan handmatig te importeren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jeśli masz hasła %@, spróbuj zaimportować je ręcznie." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se tens palavras-passe do %@, tenta importá-las manualmente." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Если у вас есть пароли в %@, попробуйте импортировать их вручную." + } + } + } + }, "import.onePassword.app.version.info" : { "comment" : "Instructions how to find an installed 1Password password manager app version.\n%1$s, %2$s - app name (1Password)", "extractionState" : "extracted_with_value", @@ -34811,7 +34931,7 @@ } }, "Passwords:" : { - "comment" : "Data import summary format of how many passwords (%lld) were successfully imported.", + "comment" : "Data import summary format of how many passwords were successfully imported.", "localizations" : { "de" : { "stringUnit" : { @@ -50946,6 +51066,7 @@ }, "Try importing bookmarks manually instead." : { "comment" : "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file.", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -50999,6 +51120,7 @@ }, "Try importing passwords manually instead." : { "comment" : "Data import error subtitle: suggestion to import Passwords manually by selecting a CSV or HTML file.", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -52129,4 +52251,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/UnitTests/DataImport/DataImportViewModelTests.swift b/UnitTests/DataImport/DataImportViewModelTests.swift index 6d698185e4..81f5fca4a7 100644 --- a/UnitTests/DataImport/DataImportViewModelTests.swift +++ b/UnitTests/DataImport/DataImportViewModelTests.swift @@ -1067,9 +1067,9 @@ import XCTest // MARK: - File Import after failure (or nil result for a data type) // all possible import summaries for combining - let bookmarksSummaries: [DataImportViewModel.DataTypeImportResult?] = [ + var bookmarksSummaries: [DataImportViewModel.DataTypeImportResult?] { // bookmarks import didn‘t happen (or skipped) - nil, + [nil, // bookmarks import succeeded .init(.bookmarks, .success(.init(successful: 42, duplicate: 3, failed: 1))), // bookmarks import succeeded with no bookmarks imported @@ -1077,8 +1077,10 @@ import XCTest // bookmarks import failed with error .init(.bookmarks, .failure(Failure(.bookmarks, .dataCorrupted))), // bookmarks import failed with file not found - .init(.bookmarks, .failure(Failure(.bookmarks, .noData))), - ] + bookmarkSummaryNoData] + } + + private let bookmarkSummaryNoData: DataImportViewModel.DataTypeImportResult? = .init(.bookmarks, .failure(Failure(.bookmarks, .noData))) let bookmarksResults: [DataImportResult?] = [ .failure(Failure(.bookmarks, .dataCorrupted)), @@ -1087,9 +1089,9 @@ import XCTest nil, // skip ] - let passwordsSummaries: [DataImportViewModel.DataTypeImportResult?] = [ + var passwordsSummaries: [DataImportViewModel.DataTypeImportResult?] { // passwords import didn‘t happen (or skipped) - nil, + [nil, // passwords import succeeded .init(.passwords, .success(.init(successful: 99, duplicate: 4, failed: 2))), // passwords import succeeded with no passwords imported @@ -1097,8 +1099,10 @@ import XCTest // passwords import failed with error .init(.passwords, .failure(Failure(.passwords, .keychainError))), // passwords import failed with file not found - .init(.passwords, .failure(Failure(.passwords, .noData))), - ] + passwordSummaryNoData] + } + + private let passwordSummaryNoData: DataImportViewModel.DataTypeImportResult? = .init(.passwords, .failure(Failure(.passwords, .noData))) let passwordsResults: [DataImportResult?] = [ .failure(Failure(.passwords, .dataCorrupted)), @@ -1128,24 +1132,19 @@ import XCTest summary: [bookmarksSummary, passwordsSummary].compactMap { $0 }) var xctDescr = "bookmarksSummary: \(bookmarksSummary?.description ?? "") passwordsSummary: \(passwordsSummary?.description ?? "") result: \(result?.description ?? ".skip")" - // run File Import (or skip) + // run File Import let expectation: DataImportViewModel if let result { try await initiateImport(of: [.bookmarks], fromFile: .testHTML, resultingWith: [.bookmarks: result], xctDescr) // expect Final Summary expectation = DataImportViewModel(importSource: source, screen: .summary([.bookmarks], isFileImport: true), summary: [bookmarksSummary, passwordsSummary, .init(.bookmarks, result)].compactMap { $0 }) - } else { - XCTAssertEqual(model.actionButton, .skip) - model.performAction(.skip) - // expect Final Summary - expectation = DataImportViewModel(importSource: source, screen: .summary([.bookmarks, .passwords]), summary: [bookmarksSummary, passwordsSummary].compactMap { $0 }) - } - xctDescr = "\(source): " + xctDescr + xctDescr = "\(source): " + xctDescr - XCTAssertEqual(model.description, expectation.description, xctDescr) - XCTAssertEqual(model.actionButton, .done, xctDescr) - XCTAssertNil(model.secondaryButton, xctDescr) + XCTAssertEqual(model.description, expectation.description, xctDescr) + XCTAssertEqual(model.actionButton, .done, xctDescr) + XCTAssertNil(model.secondaryButton, xctDescr) + } } } } @@ -1192,8 +1191,9 @@ import XCTest func testWhenBrowsersBookmarksFileImportFailsAndNoPasswordsFileImportNeeded_feedbackShown() async throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { for bookmarksSummary in bookmarksSummaries - // initial bookmark import failed or ended with empty result - where bookmarksSummary?.result.isSuccess == false || (try? bookmarksSummary?.result.get())?.isEmpty == true { + // initial bookmark import failed and is not a `noData` error, or empty + where (bookmarksSummary?.result.isSuccess == false && bookmarkSummaryNoData != bookmarksSummary) + || (try? bookmarksSummary?.result.get())?.isEmpty == true { for passwordsSummary in passwordsSummaries // passwords successfully imported @@ -1231,6 +1231,31 @@ import XCTest } } + func testWhenBrowsersBookmarksImportFailsNoDataAndFileImportSkippedAndNoPasswordsFileImportNeeded_dialogDismissed() throws { + for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { + + let bookmarksSummary = bookmarkSummaryNoData + + for passwordsSummary in passwordsSummaries + // passwords successfully imported + where (try? passwordsSummary?.result.get().isEmpty) == false { + + // setup model with pre-failed bookmarks import + setupModel(with: source, + profiles: [BrowserProfile.test, BrowserProfile.default, BrowserProfile.test2], + screen: .fileImport(dataType: .bookmarks, summary: []), + summary: [bookmarksSummary, passwordsSummary].compactMap { $0 }) + + let expectation = expectation(description: "dismissed") + model.performAction(for: .skip) { + expectation.fulfill() + } + + waitForExpectations(timeout: 0) + } + } + } + func testWhenBrowsersOnlySelectedBookmarksFileImportFails_feedbackShown() async throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker { for bookmarksSummary in bookmarksSummaries @@ -1434,8 +1459,9 @@ import XCTest for bookmarksSummary in bookmarksSummaries { for passwordsSummary in passwordsSummaries - // passwords import failed or empty - where passwordsSummary?.result.isSuccess == false || (try? passwordsSummary?.result.get().isEmpty) == false { + // passwords import failed and is not a `noData` error, or empty + where (passwordsSummary?.result.isSuccess == false && passwordSummaryNoData != passwordsSummary) + || (try? passwordsSummary?.result.get().isEmpty) == false { for bookmarksFileImportSummary in bookmarksSummaries // if bookmarks result was failure - append successful bookmarks file import result @@ -1477,6 +1503,36 @@ import XCTest } } + func testWhenBrowsersPasswordsImportFailNoDataAndFileImportSkipped_dialogDismissed() throws { + for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { + for bookmarksSummary in bookmarksSummaries { + + let passwordsSummary = passwordSummaryNoData + + for bookmarksFileImportSummary in bookmarksSummaries + // if bookmarks result was failure - append successful bookmarks file import result + where (bookmarksSummary?.result.isSuccess == false && bookmarksFileImportSummary?.result.isSuccess == true) + // if bookmarks file import summary was successful and non empty - don‘t append bookmarks file import result + // or if bookmarks file import was empty - and bookmarks file import skipped + || (bookmarksSummary?.result.isSuccess == true && bookmarksFileImportSummary == nil) { + + // setup model with pre-failed passwords import + setupModel(with: source, + profiles: [BrowserProfile.test, BrowserProfile.default, BrowserProfile.test2], + screen: .fileImport(dataType: .passwords, summary: []), + summary: [bookmarksSummary, passwordsSummary, bookmarksFileImportSummary].compactMap { $0 }) + + let expectation = expectation(description: "dismissed") + model.performAction(for: .skip) { + expectation.fulfill() + } + + waitForExpectations(timeout: 0) + } + } + } + } + func testWhenBrowsersOnlySelectedPasswordsFileImportFails_feedbackShown() async throws { for source in Source.allCases where source.initialScreen == .profileAndDataTypesPicker && source.supportedDataTypes.contains(.passwords) { for passwordsSummary in passwordsSummaries diff --git a/UnitTests/DataImport/FirefoxBookmarksReaderTests.swift b/UnitTests/DataImport/FirefoxBookmarksReaderTests.swift index 0a4ffdc2ee..f175f59f1b 100644 --- a/UnitTests/DataImport/FirefoxBookmarksReaderTests.swift +++ b/UnitTests/DataImport/FirefoxBookmarksReaderTests.swift @@ -39,9 +39,26 @@ class FirefoxBookmarksReaderTests: XCTestCase { }), true) } + func testFileNotFoundReturnsFailureWithDbOpenError() { + // Given + let bookmarksReader = FirefoxBookmarksReader(firefoxDataDirectoryURL: invalidResourceURL()) + let expected: DataImportResult = .failure(FirefoxBookmarksReader.ImportError(type: .couldNotFindBookmarksFile, underlyingError: nil)) + + // When + let result = bookmarksReader.readBookmarks() + + // Then + XCTAssertEqual(expected, result) + } + private func resourceURL() -> URL { let bundle = Bundle(for: FirefoxBookmarksReaderTests.self) return bundle.resourceURL!.appendingPathComponent("DataImportResources/TestFirefoxData") } + private func invalidResourceURL() -> URL { + let bundle = Bundle(for: FirefoxBookmarksReaderTests.self) + return bundle.resourceURL!.appendingPathComponent("Nothing/Here") + } + } diff --git a/UnitTests/DataImport/FirefoxLoginReaderTests.swift b/UnitTests/DataImport/FirefoxLoginReaderTests.swift index 83abe67b66..cdbaa31662 100644 --- a/UnitTests/DataImport/FirefoxLoginReaderTests.swift +++ b/UnitTests/DataImport/FirefoxLoginReaderTests.swift @@ -132,7 +132,7 @@ class FirefoxLoginReaderTests: XCTestCase { switch result { case .failure(let error as FirefoxLoginReader.ImportError): - XCTAssertEqual(error.type, .couldNotDetermineFormat) + XCTAssertEqual(error.type, .couldNotFindKeyDB) default: XCTFail("Received unexpected \(result)") } @@ -274,6 +274,26 @@ class FirefoxLoginReaderTests: XCTestCase { } } + func testWhenImportingLogins_AndNoKeysDBExists_ThenImportFailsWithNoDBError() throws { + // Given + let logins = resourcesURLWithoutPassword().appendingPathComponent("logins.json") + + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { + File("logins.json", contents: .copy(logins)) + } + + try structure.writeToTemporaryDirectory() + let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) + + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL) + + // When + let result = firefoxLoginReader.readLogins(dataFormat: nil) + + // Then + XCTAssertEqual(result, .failure(FirefoxLoginReader.ImportError(type: .couldNotFindKeyDB, underlyingError: nil))) + } + private func resourcesURLWithPassword() -> URL { let bundle = Bundle(for: FirefoxLoginReaderTests.self) return bundle.resourceURL!.appendingPathComponent("DataImportResources/TestFirefoxData/Primary Password") From 2d3a46c4863a4dd18be360a01d503572240e75b3 Mon Sep 17 00:00:00 2001 From: Halle <378795+Halle@users.noreply.github.com> Date: Thu, 4 Apr 2024 03:24:23 -0700 Subject: [PATCH 007/221] Adds a series of UI tests for Bookmarks Bar visibility Task/Issue URL: https://app.asana.com/0/1199230911884351/1205717021705368/f Tech Design URL: CC: **Description**: Adds a series of UI tests for Bookmarks Bar visibility **Steps to test this PR**: 1. Open the scheme **UI Tests** 2. Navigate to the test pane 3. Run BookmarksBarTests Note: this is the first PR where the scheme builds `review`, so it should no longer be necessary to comment out `app.launchEnvironment["UITEST_MODE"] = "1"`. However, if this builds a `review` build that is treated as a first run on your system during every test, please build and run the scheme once instead of running its tests, and go through the first run steps once before running the tests (I will check in with @kshann about how to deal with this in CI, or if it is necessary to do so). --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../xcshareddata/xcschemes/UI Tests.xcscheme | 6 +- .../Dialog/BookmarkDialogButtonsView.swift | 2 +- .../View/BookmarksBarViewController.swift | 1 + .../View/PreferencesAppearanceView.swift | 4 + .../Preferences/View/PreferencesSidebar.swift | 1 + UITests/AutocompleteTests.swift | 17 +- UITests/BookmarksBarTests.swift | 221 ++++++++++++++++++ UITests/BrowsingHistoryTests.swift | 8 +- UITests/Common/UITests.swift | 1 + UITests/Common/XCUIElementExtension.swift | 25 +- UITests/FindInPageTests.swift | 41 +++- 12 files changed, 300 insertions(+), 31 deletions(-) create mode 100644 UITests/BookmarksBarTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 78225c14cb..9d9985b44c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3275,6 +3275,7 @@ EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295E82A545BC4008C0991 /* NetworkProtection */; }; EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EC2A545C0A008C0991 /* NetworkProtection */; }; EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EE2A545C12008C0991 /* NetworkProtection */; }; + EE7F74912BB5D76600CD9456 /* BookmarksBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */; }; EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; @@ -4736,6 +4737,7 @@ EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; + EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarTests.swift; sourceTree = ""; }; EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLauncher.swift; sourceTree = ""; }; @@ -6515,6 +6517,7 @@ children = ( EEBCE6802BA444FA00B9DF00 /* Common */, EED735352BB46B6000F173D6 /* AutocompleteTests.swift */, + EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */, EE02D41B2BB460A600DBE6B3 /* BrowsingHistoryTests.swift */, EE0429DF2BA31D2F009EB20F /* FindInPageTests.swift */, 7B4CE8E626F02134009134B1 /* TabBarTests.swift */, @@ -12206,6 +12209,7 @@ buildActionMask = 2147483647; files = ( EEBCE6842BA4643200B9DF00 /* NSSizeExtension.swift in Sources */, + EE7F74912BB5D76600CD9456 /* BookmarksBarTests.swift in Sources */, EE02D41C2BB460A600DBE6B3 /* BrowsingHistoryTests.swift in Sources */, EE02D41A2BB4609900DBE6B3 /* UITests.swift in Sources */, EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme index ce215006ce..5ded4d9621 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme @@ -23,9 +23,9 @@ diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift index d55f6dd34f..80e9320dc2 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogButtonsView.swift @@ -40,7 +40,7 @@ struct BookmarkDialogButtonsView: View { Spacer() } - actionButton(action: otherButtonAction, viewState: viewState) + actionButton(action: otherButtonAction, viewState: viewState).accessibilityIdentifier("BookmarkDialogButtonsView.otherButton") actionButton(action: defaultButtonAction, viewState: viewState).accessibilityIdentifier("BookmarkDialogButtonsView.defaultButton") } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index e298a6335e..b927a05751 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -82,6 +82,7 @@ final class BookmarksBarViewController: NSViewController { bookmarksBarCollectionView.collectionViewLayout = createCenteredCollectionViewLayout() view.postsFrameChangedNotifications = true + bookmarksBarCollectionView.setAccessibilityIdentifier("BookmarksBarViewController.bookmarksBarCollectionView") } private func addContextMenu() { diff --git a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift index be30d68da2..a7b30dae90 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift @@ -115,15 +115,19 @@ extension Preferences { HStack { ToggleMenuItem(UserText.showBookmarksBarPreference, isOn: $model.showBookmarksBar) + .accessibilityIdentifier("Preferences.AppearanceView.showBookmarksBarPreferenceToggle") NSPopUpButtonView(selection: $model.bookmarksBarAppearance) { let button = NSPopUpButton() button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + button.setAccessibilityIdentifier("Preferences.AppearanceView.showBookmarksBarPopUp") let alwaysOn = button.menu?.addItem(withTitle: UserText.showBookmarksBarAlways, action: nil, keyEquivalent: "") alwaysOn?.representedObject = BookmarksBarAppearance.alwaysOn + alwaysOn?.setAccessibilityIdentifier("Preferences.AppearanceView.showBookmarksBarAlways") let newTabOnly = button.menu?.addItem(withTitle: UserText.showBookmarksBarNewTabOnly, action: nil, keyEquivalent: "") newTabOnly?.representedObject = BookmarksBarAppearance.newTabOnly + newTabOnly?.setAccessibilityIdentifier("Preferences.AppearanceView.showBookmarksBarNewTabOnly") return button } diff --git a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift index b6f65c0a44..1b234a1c49 100644 --- a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift +++ b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift @@ -66,6 +66,7 @@ extension Preferences { } } .buttonStyle(SidebarItemButtonStyle(isSelected: isSelected)) + .accessibilityIdentifier("PreferencesSidebar.\(pane.id.rawValue)Button") } } diff --git a/UITests/AutocompleteTests.swift b/UITests/AutocompleteTests.swift index 2b02369b58..2ef08b7e8c 100644 --- a/UITests/AutocompleteTests.swift +++ b/UITests/AutocompleteTests.swift @@ -54,7 +54,6 @@ class AutocompleteTests: XCTestCase { } func test_suggestions_showsTypedTitleOfBookmarkedPageAsBookmark() throws { - let siteTitleForBookmarkedSite = try XCTUnwrap(siteTitleForBookmarkedSite) app.typeText(siteTitleForBookmarkedSite) XCTAssertTrue( suggestionsTableView.waitForExistence(timeout: UITests.Timeouts.elementExistence), @@ -82,7 +81,6 @@ class AutocompleteTests: XCTestCase { } func test_suggestions_showsTypedTitleOfHistoryPageAsHistory() throws { - let siteTitleForHistorySite = try XCTUnwrap(siteTitleForHistorySite) app.typeText(siteTitleForHistorySite) XCTAssertTrue( suggestionsTableView.waitForExistence(timeout: UITests.Timeouts.elementExistence), @@ -110,18 +108,20 @@ class AutocompleteTests: XCTestCase { } func test_suggestions_showsTypedTitleOfWebsiteNotInBookmarksOrHistoryAsWebsite() throws { - let websiteURLString = "https://www.duckduckgo.com" + let websiteString = "https://www.duckduckgo.com" + let websiteURL = try XCTUnwrap(URL(string: websiteString), "Couldn't create URL from string \(websiteString)") XCTAssertTrue( addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The address bar text field didn't become available in a reasonable timeframe." ) - addressBarTextField.typeText(websiteURLString) + + addressBarTextField.typeURL(websiteURL, pressingEnter: false) XCTAssertTrue( suggestionsTableView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Suggestions tableView didn't become available in a reasonable timeframe." ) - let suggestionCellWithWebsite = suggestionsTableView.tableRows.cells.staticTexts[websiteURLString].firstMatch + let suggestionCellWithWebsite = suggestionsTableView.tableRows.cells.staticTexts[websiteURL.absoluteString].firstMatch XCTAssertTrue( // It should match something in suggestions suggestionCellWithWebsite.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The expected table view cell with the suggestion for the website didn't become available in a reasonable timeframe." @@ -145,9 +145,6 @@ class AutocompleteTests: XCTestCase { private extension AutocompleteTests { /// Make sure there is exactly one site in the history, and exactly one site in the bookmarks, and they aren't the same site. func resetAndArrangeBookmarksAndHistory() throws { - let siteTitleForHistorySite = try XCTUnwrap(siteTitleForHistorySite) - let siteTitleForBookmarkedSite = try XCTUnwrap(siteTitleForBookmarkedSite) - XCTAssertTrue( resetBookMarksMenuItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Reset bookmarks menu item didn't become available in a reasonable timeframe." @@ -160,7 +157,7 @@ private extension AutocompleteTests { "The address bar text field didn't become available in a reasonable timeframe." ) - addressBarTextField.typeText("\(urlForBookmarks.absoluteString)\r") + addressBarTextField.typeURL(urlForBookmarks) XCTAssertTrue( app.windows.webViews[siteTitleForBookmarkedSite].waitForExistence(timeout: UITests.Timeouts.elementExistence), "Visited site didn't load with the expected title in a reasonable timeframe." @@ -202,7 +199,7 @@ private extension AutocompleteTests { "The address bar text field didn't become available in a reasonable timeframe." ) - addressBarTextField.typeText("\(urlForHistory.absoluteString)\r") + addressBarTextField.typeURL(urlForHistory) XCTAssertTrue( app.windows.webViews[siteTitleForHistorySite].waitForExistence(timeout: UITests.Timeouts.elementExistence), "Visited site didn't load with the expected title in a reasonable timeframe." diff --git a/UITests/BookmarksBarTests.swift b/UITests/BookmarksBarTests.swift new file mode 100644 index 0000000000..55eecbe52b --- /dev/null +++ b/UITests/BookmarksBarTests.swift @@ -0,0 +1,221 @@ +// +// BookmarksBarTests.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 Common +import XCTest + +class BookmarksBarTests: XCTestCase { + private var app: XCUIApplication! + private var pageTitle: String! + private var urlForBookmarksBar: URL! + private var settingsWindow: XCUIElement! + private var siteWindow: XCUIElement! + private var defaultBookmarkDialogButton: XCUIElement! + private var resetBookMarksMenuItem: XCUIElement! + private var showBookmarksBarPreferenceToggle: XCUIElement! + private var showBookmarksBarPopup: XCUIElement! + private var showBookmarksBarAlways: XCUIElement! + private var showBookmarksBarNewTabOnly: XCUIElement! + private var bookmarksBarCollectionView: XCUIElement! + private var addressBarTextField: XCUIElement! + private let titleStringLength = 12 + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" + defaultBookmarkDialogButton = app.buttons["BookmarkDialogButtonsView.defaultButton"] + showBookmarksBarPreferenceToggle = app.checkBoxes["Preferences.AppearanceView.showBookmarksBarPreferenceToggle"] + resetBookMarksMenuItem = app.menuItems["MainMenu.resetBookmarks"] + showBookmarksBarPopup = app.popUpButtons["Preferences.AppearanceView.showBookmarksBarPopUp"] + showBookmarksBarAlways = app.menuItems["Preferences.AppearanceView.showBookmarksBarAlways"] + showBookmarksBarNewTabOnly = app.menuItems["Preferences.AppearanceView.showBookmarksBarNewTabOnly"] + bookmarksBarCollectionView = app.collectionViews["BookmarksBarViewController.bookmarksBarCollectionView"] + addressBarTextField = app.windows.textFields["AddressBarViewController.addressBarTextField"] + pageTitle = UITests.randomPageTitle(length: titleStringLength) + urlForBookmarksBar = UITests.simpleServedPage(titled: pageTitle) + app.launch() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows + app.typeKey("n", modifierFlags: [.command]) // Guarantee a single window + resetBookmarksAndAddOneBookmark() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows + openSettingsAndSetShowBookmarksBarToUnchecked() + settingsWindow = app.windows.containing(.checkBox, identifier: "Preferences.AppearanceView.showBookmarksBarPreferenceToggle").firstMatch + openSecondWindowAndVisitSite() + siteWindow = app.windows.containing(.webView, identifier: pageTitle).firstMatch + } + + func test_bookmarksBar_whenShowBookmarksBarAlwaysIsSelected_alwaysDynamicallyAppearsOnWindow() throws { + app.typeKey("w", modifierFlags: [.command]) + XCTAssertTrue( + showBookmarksBarPreferenceToggle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The toggle for showing the bookmarks bar didn't become available in a reasonable timeframe." + ) + + let showBookmarksBarIsChecked = try? XCTUnwrap( + showBookmarksBarPreferenceToggle.value as? Bool, + "It wasn't possible to get the \"Show bookmarks bar\" value as a Bool" + ) + if showBookmarksBarIsChecked == false { + showBookmarksBarPreferenceToggle.click() + } + XCTAssertTrue( + showBookmarksBarPopup.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Show Bookmarks Bar\" popup button didn't become available in a reasonable timeframe." + ) + showBookmarksBarPopup.click() + XCTAssertTrue( + showBookmarksBarAlways.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Show Bookmarks Bar Always\" button didn't become available in a reasonable timeframe." + ) + showBookmarksBarAlways.click() + + XCTAssertTrue( + bookmarksBarCollectionView.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarksBarCollectionView should exist on a website window when we have selected \"Always Show Bookmarks Bar\" in the settings" + ) + } + + func test_bookmarksBar_whenShowBookmarksNewTabOnlyIsSelected_onlyAppearsOnANewTabUntilASiteIsLoaded() throws { + app.typeKey("w", modifierFlags: [.command]) // Close site window + XCTAssertTrue( + showBookmarksBarPreferenceToggle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The toggle for showing the bookmarks bar didn't become available in a reasonable timeframe." + ) + + let showBookmarksBarIsChecked = try? XCTUnwrap( + showBookmarksBarPreferenceToggle.value as? Bool, + "It wasn't possible to get the \"Show bookmarks bar\" value as a Bool" + ) + if showBookmarksBarIsChecked == false { + showBookmarksBarPreferenceToggle.click() + } + XCTAssertTrue( + showBookmarksBarPopup.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Show Bookmarks Bar\" popup button didn't become available in a reasonable timeframe." + ) + showBookmarksBarPopup.click() + + XCTAssertTrue( + showBookmarksBarNewTabOnly.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Show Bookmarks Bar Always\" button didn't become available in a reasonable timeframe." + ) + showBookmarksBarNewTabOnly.click() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows + app.typeKey("n", modifierFlags: [.command]) // open one new window + + XCTAssertTrue( + bookmarksBarCollectionView.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarksBarCollectionView should exist on a new tab into which no site name or location has been typed yet." + ) + XCTAssertTrue( + addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The Address Bar text field did not exist when it was expected." + ) + addressBarTextField.typeURL(urlForBookmarksBar) + XCTAssertTrue( + bookmarksBarCollectionView.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarksBarCollectionView should not exist on a tab that has been directed to a site, and is no longer new, when we have selected show bookmarks bar \"New Tab Only\" in the settings" + ) + } + + func test_bookmarksBar_whenShowBookmarksBarIsUnchecked_isNeverShownInWindowsAndTabs() throws { + // This tests begins in the state that "show bookmarks bar" is unchecked, so that isn't set within the test + + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows + app.typeKey("n", modifierFlags: [.command]) // Open new window + XCTAssertTrue( + bookmarksBarCollectionView.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarksBarCollectionView should not exist on a new window when we have unchecked \"Show Bookmarks Bar\" in the settings" + ) + + app.typeKey("t", modifierFlags: [.command]) // Open new tab + XCTAssertTrue( + bookmarksBarCollectionView.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarksBarCollectionView should not exist on a new tab when we have unchecked \"Show Bookmarks Bar\" in the settings" + ) + app.typeKey("l", modifierFlags: [.command]) // Get address bar focus + app.typeURL(urlForBookmarksBar) + + XCTAssertTrue( + bookmarksBarCollectionView.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarksBarCollectionView should not exist on a new tab that has been directed to a site when we have unchecked \"Show Bookmarks Bar\" in the settings" + ) + } +} + +private extension BookmarksBarTests { + func openSettingsAndSetShowBookmarksBarToUnchecked() { + app.typeKey(",", modifierFlags: [.command]) + + let settingsAppearanceButton = app.buttons["PreferencesSidebar.appearanceButton"] + XCTAssertTrue( + settingsAppearanceButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The user settings appearance section button didn't become available in a reasonable timeframe." + ) + // This should just be a click(), but there are states for this test where the first few clicks don't register here. + settingsAppearanceButton.click(forDuration: UITests.Timeouts.elementExistence, thenDragTo: settingsAppearanceButton) + + XCTAssertTrue( + showBookmarksBarPreferenceToggle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The toggle for showing the bookmarks bar didn't become available in a reasonable timeframe." + ) + + let showBookmarksBarIsChecked = showBookmarksBarPreferenceToggle.value as? Bool + if showBookmarksBarIsChecked == true { + showBookmarksBarPreferenceToggle.click() + } + } + + func openSecondWindowAndVisitSite() { + app.typeKey("n", modifierFlags: [.command]) + app.typeKey("l", modifierFlags: [.command]) // Get address bar focus without addressing multiple address bars by identifier + XCTAssertTrue( // Use home page logo as a test to know if a new window is fully ready before we type + app.images["HomePageLogo"].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The Home Page Logo did not exist when it was expected." + ) + app.typeURL(urlForBookmarksBar) + } + + func resetBookmarksAndAddOneBookmark() { + XCTAssertTrue( + resetBookMarksMenuItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Reset bookmarks menu item didn't become available in a reasonable timeframe." + ) + + resetBookMarksMenuItem.click() + XCTAssertTrue( + addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The Address Bar text field did not exist when it was expected." + ) + addressBarTextField.typeURL(urlForBookmarksBar) + XCTAssertTrue( + app.windows.webViews[pageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Visited site didn't load with the expected title in a reasonable timeframe." + ) + + app.typeKey("d", modifierFlags: [.command]) // Bookmark the page + + XCTAssertTrue( + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmark button didn't appear with the expected title in a reasonable timeframe." + ) + + defaultBookmarkDialogButton.click() + } +} diff --git a/UITests/BrowsingHistoryTests.swift b/UITests/BrowsingHistoryTests.swift index 53aaade7e5..2218bd9d32 100644 --- a/UITests/BrowsingHistoryTests.swift +++ b/UITests/BrowsingHistoryTests.swift @@ -72,7 +72,7 @@ class BrowsingHistoryTests: XCTestCase { "The address bar text field didn't become available in a reasonable timeframe." ) - addressBarTextField.typeText("\(url.absoluteString)\r") + addressBarTextField.typeURL(url) XCTAssertTrue( app.windows.webViews[historyPageTitleExpectedToBeFirstInRecentlyVisited] .waitForExistence(timeout: UITests.Timeouts.elementExistence), @@ -100,7 +100,7 @@ class BrowsingHistoryTests: XCTestCase { "The address bar text field didn't become available in a reasonable timeframe." ) - addressBarTextField.typeText("\(url.absoluteString)\r") + addressBarTextField.typeURL(url) XCTAssertTrue( app.windows.webViews[historyPageTitleExpectedToBeFirstInTodayHistory].waitForExistence(timeout: UITests.Timeouts.elementExistence), "Visited site didn't load with the expected title in a reasonable timeframe." @@ -129,14 +129,14 @@ class BrowsingHistoryTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The address bar text field didn't become available in a reasonable timeframe." ) - addressBarTextField.typeText("\(urlForFirstTab.absoluteString)\r") + addressBarTextField.typeURL(urlForFirstTab) XCTAssertTrue( app.windows.webViews[titleOfFirstTabWhichShouldRestore].waitForExistence(timeout: UITests.Timeouts.elementExistence), "Visited site didn't load with the expected title in a reasonable timeframe." ) app.typeKey("t", modifierFlags: .command) - addressBarTextField.typeText("\(urlForSecondTab.absoluteString)\r") + addressBarTextField.typeURL(urlForSecondTab) XCTAssertTrue( app.windows.webViews[titleOfSecondTabWhichShouldRestore].waitForExistence(timeout: UITests.Timeouts.elementExistence), "Visited site didn't load with the expected title in a reasonable timeframe." diff --git a/UITests/Common/UITests.swift b/UITests/Common/UITests.swift index 3d97fb6a96..40f28936f9 100644 --- a/UITests/Common/UITests.swift +++ b/UITests/Common/UITests.swift @@ -17,6 +17,7 @@ // import Foundation +import XCTest /// Helper values for the UI tests enum UITests { diff --git a/UITests/Common/XCUIElementExtension.swift b/UITests/Common/XCUIElementExtension.swift index 8558cd9a7e..7e72c41a02 100644 --- a/UITests/Common/XCUIElementExtension.swift +++ b/UITests/Common/XCUIElementExtension.swift @@ -19,7 +19,6 @@ import XCTest extension XCUIElement { - // https://stackoverflow.com/a/63089781/119717 // Licensed under https://creativecommons.org/licenses/by-sa/4.0/ // Credit: Adil Hussain @@ -39,4 +38,28 @@ extension XCUIElement { return false } + + /// On some individual systems, strings which contain a ":" do not type the ":" when the string is entirely typed with `typeText(...)` into the + /// address bar, + /// wherever the ":" occurs in the string. This function stops before the ":" character and then types it with `typeKey(...)` as a workaround for + /// this bug or unknown system setting. + /// - Parameters: + /// - url: The URL to be typed into the address bar + /// - pressingEnter: If the `enter` key should not be pressed after typing this URL in, set this optional parameter to `false`, otherwise it + /// will be pressed. + func typeURL(_ url: URL, pressingEnter: Bool = true) { + let urlString = url.absoluteString + let urlParts = urlString.split(separator: ":") + var completedURLSections = 0 + for urlPart in urlParts { + self.typeText(String(urlPart)) + completedURLSections += 1 + if completedURLSections != urlParts.count { + self.typeKey(":", modifierFlags: []) + } + } + if pressingEnter { + self.typeText("\r") + } + } } diff --git a/UITests/FindInPageTests.swift b/UITests/FindInPageTests.swift index f3337f0cc7..bb8f124f10 100644 --- a/UITests/FindInPageTests.swift +++ b/UITests/FindInPageTests.swift @@ -50,7 +50,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -69,7 +69,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -93,7 +93,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -120,7 +120,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -144,7 +144,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -168,7 +168,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -197,7 +197,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -224,7 +224,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -259,7 +259,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -318,7 +318,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -376,7 +376,7 @@ class FindInPageTests: XCTestCase { addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), "The Address Bar text field did not exist when it was expected." ) - addressBarTextField.typeText("\(Self.loremIpsumFileURL.absoluteString)\r") + addressBarTextField.typeURL(Self.loremIpsumFileURL) XCTAssertTrue( loremIpsumWebView.waitForExistence(timeout: UITests.Timeouts.elementExistence), "Local \"Lorem Ipsum\" web page didn't load with the expected title in a reasonable timeframe. If this is unexpected, it can also be due to the timeout being too short." @@ -469,6 +469,21 @@ private extension FindInPageTests { } } +private extension UInt8 { + func isCloseTo(_ colorValue: UInt8) -> Bool { + // Overflow-safe creation of range +/- 1 around value + let lowerBound: UInt8 = self != 0 ? self &- 1 : 0 + let upperBound: UInt8 = self != 255 ? self &+ 1 : 255 + + switch colorValue { + case lowerBound ... upperBound: + return true + default: + return false + } + } +} + private extension NSImage { /// Find matching pixels in an NSImage for a specific NSColor /// - Parameter colorToMatch: the NSColor to match @@ -506,7 +521,9 @@ private extension NSImage { bitmapData = bitmapData.advanced(by: 1) alphaInImage = bitmapData.pointee bitmapData = bitmapData.advanced(by: 1) - if redInImage == redToMatch, greenInImage == greenToMatch, blueInImage == blueToMatch { // We aren't matching alpha + if redInImage.isCloseTo(redToMatch), greenInImage.isCloseTo(greenToMatch), + blueInImage.isCloseTo(blueToMatch) + { // We aren't matching alpha pixels.append(Pixel( red: redInImage, green: greenInImage, From 346e8956d724fcec99d6e84015e40be7c308df90 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 4 Apr 2024 09:03:05 -0300 Subject: [PATCH 008/221] DBP: Compare by url and not name (#2544) --- .../ParentChildRelationship/MismatchCalculatorUseCase.swift | 2 +- .../MismatchCalculatorUseCaseTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift index ad3e963fe3..9f278ab31a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift @@ -47,7 +47,7 @@ struct MismatchCalculatorUseCase { for parent in parentBrokerProfileQueryData { guard let parentMatches = parent.scanOperationData.historyEvents.matchesForLastEvent() else { continue } let children = brokerProfileQueryData.filter { - $0.dataBroker.parent == parent.dataBroker.name && + $0.dataBroker.parent == parent.dataBroker.url && $0.profileQuery.id == parent.profileQuery.id } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index db0607d891..f10a26fb67 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -170,7 +170,7 @@ extension BrokerProfileQueryData { steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, - parent: "parent" + parent: "parent.com" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), scanOperationData: ScanOperationData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) From 896e80396e3913129aebdf604c3082b6c93b4775 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 4 Apr 2024 09:03:05 -0300 Subject: [PATCH 009/221] DBP: Compare by url and not name (#2544) --- .../ParentChildRelationship/MismatchCalculatorUseCase.swift | 2 +- .../MismatchCalculatorUseCaseTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift index ad3e963fe3..9f278ab31a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift @@ -47,7 +47,7 @@ struct MismatchCalculatorUseCase { for parent in parentBrokerProfileQueryData { guard let parentMatches = parent.scanOperationData.historyEvents.matchesForLastEvent() else { continue } let children = brokerProfileQueryData.filter { - $0.dataBroker.parent == parent.dataBroker.name && + $0.dataBroker.parent == parent.dataBroker.url && $0.profileQuery.id == parent.profileQuery.id } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index db0607d891..f10a26fb67 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -170,7 +170,7 @@ extension BrokerProfileQueryData { steps: [Step](), version: "1.0.0", schedulingConfig: DataBrokerScheduleConfig.mock, - parent: "parent" + parent: "parent.com" ), profileQuery: ProfileQuery(firstName: "John", lastName: "Doe", city: "Miami", state: "FL", birthYear: 50), scanOperationData: ScanOperationData(brokerId: 2, profileQueryId: 1, historyEvents: historyEvents) From 38820b4709978a64c3869a2a70525c79c86dcfc5 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Thu, 4 Apr 2024 07:56:18 -0500 Subject: [PATCH 010/221] Update Neighbor Report (#2542) Task/Issue URL: https://app.asana.com/0/0/1206998831911062/f Tech Design URL: CC: **Description**: Sometimes Neighbor Report fails to load the page after the opt out form (where buttons need to be clicked). When this happens, we were reporting errors on the "click" action which was confusing for those needing to debug. This PR adds an expectation to check and make sure we're looking at the correct page before clicking buttons. --- .../Resources/JSON/neighbor.report.json | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json index 8e09a3db50..e1a8c51741 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON/neighbor.report.json @@ -1,7 +1,7 @@ { "name": "Neighbor Report", "url": "neighbor.report", - "version": "0.1.5", + "version": "0.1.6", "addedDatetime": 1703570400000, "steps": [ { @@ -10,12 +10,12 @@ "actions": [ { "actionType": "navigate", - "id": "22f67a44-d538-48a0-b6f4-c783123c6dfa", + "id": "6e589101-df53-4af7-bf6a-41376927f6ec", "url": "https://neighbor.report/${firstName}-${lastName}/${state|stateFull|hyphenated}/${city|hyphenated}" }, { "actionType": "extract", - "id": "14ff868b-b6cc-485e-a5cf-93357a63c2ac", + "id": "1ec79c67-40b0-4f91-baf8-996073109092", "selector": ".lstd", "profile": { "name": { @@ -53,12 +53,12 @@ "actions": [ { "actionType": "navigate", - "id": "477f4e90-686c-400e-8d45-65749ff60b5d", + "id": "d894a462-02c3-4f2a-966d-44aea2fbb6d4", "url": "https://neighbor.report/remove" }, { "actionType": "fillForm", - "id": "335b078c-d27f-4045-8a36-2cc0c125d271", + "id": "7fbaf65f-c130-494b-8f61-a77cebb422f4", "selector": ".form-horizontal", "elements": [ { @@ -77,17 +77,17 @@ }, { "actionType": "getCaptchaInfo", - "id": "0ed843a1-2788-454c-a666-17906d2991cb", + "id": "23d6eb3e-2cfd-45f8-a0ca-b2168605173e", "selector": ".recaptcha-div" }, { "actionType": "solveCaptcha", - "id": "e6f3b3c5-e61c-4e75-878d-cecbaae26013", + "id": "f0ea9801-8b8d-4e27-bd3f-bba0cdb4bf4a", "selector": ".recaptcha-div" }, { "actionType": "click", - "id": "8544dcb5-5bd7-4045-91a8-23bbfad7bfc8", + "id": "2aa19f1d-1dbf-4465-8e40-857311bf7f37", "elements": [ { "type": "button", @@ -95,9 +95,20 @@ } ] }, + { + "actionType": "expectation", + "id": "2f4f23f8-4599-48a6-a52c-3ed630114a90", + "expectations": [ + { + "type": "text", + "selector": "body", + "expect": "Remove persons" + } + ] + }, { "actionType": "click", - "id": "6748ca30-0044-4e9a-8a35-5d686359688c", + "id": "1f411b2e-9f93-4519-ae35-7c0411b535e1", "elements": [ { "type": "button", @@ -111,7 +122,7 @@ }, { "actionType": "expectation", - "id": "a56fcc4c-fdd4-4d92-96b3-a8b89d10ca80", + "id": "2bf0a2dd-7d16-4d40-9091-61c96a1e6949", "expectations": [ { "type": "text", From 35c725da5cae30535eb706b78534ca1ed5328610 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Thu, 4 Apr 2024 14:27:11 +0100 Subject: [PATCH 011/221] Fix usertext comment to ensure it matches localizable string (#2546) Updates UserText string with comment to match localizable file comment. --- .../xcschemes/DuckDuckGo Privacy Browser.xcscheme | 3 +++ DuckDuckGo/Common/Localizables/UserText.swift | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index d683d41f9e..497c10ee86 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -126,6 +126,9 @@ + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index e78ee81e96..49a5fb4d84 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -637,10 +637,10 @@ struct UserText { } static func importNoDataBookmarksSubtitle(from source: DataImport.Source) -> String { - String(format: NSLocalizedString("import.nodata.bookmarks.subtitle", value: "If you have %@ bookmarks, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file."), source.importSourceName) + String(format: NSLocalizedString("import.nodata.bookmarks.subtitle", value: "If you have %@ bookmarks, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox."), source.importSourceName) } static func importNoDataPasswordsSubtitle(from source: DataImport.Source) -> String { - String(format: NSLocalizedString("import.nodata.passwords.subtitle", value: "If you have %@ passwords, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import passwords manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox"), source.importSourceName) + String(format: NSLocalizedString("import.nodata.passwords.subtitle", value: "If you have %@ passwords, try importing them manually instead.", comment: "Data import error subtitle: suggestion to import passwords manually by selecting a CSV or HTML file. The placeholder here represents the source browser, e.g Firefox."), source.importSourceName) } static let importLoginsPasswords = NSLocalizedString("import.logins.passwords", value: "Passwords", comment: "Title text for the Passwords import option") From 2b99d0eb0b9a5047c459d75b506d7a72de570853 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 4 Apr 2024 18:54:13 +0500 Subject: [PATCH 012/221] add LSHandlerRank for .duckload document type (#2537) Task/Issue URL: https://app.asana.com/0/1201037661562251/1206980087581641/f --- DuckDuckGo/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index a521153891..45285233da 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -43,6 +43,8 @@ Editor NSIsRelatedItemType + LSHandlerRank + Owner CFBundleExecutable From 3c86a0fd3b6522977154b05e5b6714a13e5c1f59 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Thu, 4 Apr 2024 15:53:12 +0100 Subject: [PATCH 013/221] Pixel changed in subscription (#2541) Task/Issue URL:https://app.asana.com/0/1205842942115003/1205469290776415/f **Description**: Pixel replaced in Subscription --- DuckDuckGo/Preferences/View/PreferencesRootView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 922b2fe501..d550717a5f 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -160,7 +160,7 @@ enum Preferences { case .activateAddEmailClick: DailyPixel.fire(pixel: .privacyProRestorePurchaseEmailStart, frequency: .dailyAndCount) case .postSubscriptionAddEmailClick: - Pixel.fire(.privacyProWelcomeAddDevice, limitTo: .initial) + Pixel.fire(.privacyProSubscriptionManagementEmail, limitTo: .initial) case .restorePurchaseStoreClick: DailyPixel.fire(pixel: .privacyProRestorePurchaseStoreStart, frequency: .dailyAndCount) case .addToAnotherDeviceClick: From dfd00ec628e47784509663d3a5130935b8b9a43b Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 4 Apr 2024 20:35:21 +0000 Subject: [PATCH 014/221] Bump version to 1.82.0 (152) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index f2874c8de9..92feb2ccd4 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 151 +CURRENT_PROJECT_VERSION = 152 From eefb7a458f36ba6e3bfa14860d2ed42ec92a83fa Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Fri, 5 Apr 2024 09:39:45 +1100 Subject: [PATCH 015/221] Add CI support for handling installation attribution (#2502) Task/Issue URL: https://app.asana.com/0/0/1206923080258472/f **Description**: This PR adds support for attributing installs without measuring retention. --- .../asana-get-build-variants-list/action.yml | 36 +++ .../get_build_variants_list.sh | 110 +++++++++ .github/workflows/create_variant.yml | 201 +++++++++++++++++ .github/workflows/create_variants.yml | 210 +++++------------- DuckDuckGo.xcodeproj/project.pbxproj | 52 +++++ .../ATB/AttributionOriginFileProvider.swift | 47 ++++ .../InstallationAttributionPixelHandler.swift | 87 ++++++++ .../Statistics/ATB/StatisticsLoader.swift | 9 +- DuckDuckGo/Statistics/PixelEvent.swift | 6 + DuckDuckGo/Statistics/PixelParameters.swift | 3 +- .../AttributionOriginFileProviderTests.swift | 61 +++++ ...allationAttributionPixelHandlerTests.swift | 124 +++++++++++ .../Mock/MockAttributionOriginProvider.swift | 28 +++ .../Mock/MockAttributionsPixelHandler.swift | 30 +++ .../Statistics/ATB/Mock/Origin-empty.txt | 0 UnitTests/Statistics/ATB/Mock/Origin.txt | 1 + .../ATB/StatisticsLoaderTests.swift | 60 ++++- 17 files changed, 904 insertions(+), 161 deletions(-) create mode 100644 .github/actions/asana-get-build-variants-list/action.yml create mode 100755 .github/actions/asana-get-build-variants-list/get_build_variants_list.sh create mode 100644 .github/workflows/create_variant.yml create mode 100644 DuckDuckGo/Statistics/ATB/AttributionOriginFileProvider.swift create mode 100644 DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift create mode 100644 UnitTests/Statistics/ATB/AttributionOriginFileProviderTests.swift create mode 100644 UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift create mode 100644 UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift create mode 100644 UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift create mode 100644 UnitTests/Statistics/ATB/Mock/Origin-empty.txt create mode 100644 UnitTests/Statistics/ATB/Mock/Origin.txt diff --git a/.github/actions/asana-get-build-variants-list/action.yml b/.github/actions/asana-get-build-variants-list/action.yml new file mode 100644 index 0000000000..38543f750a --- /dev/null +++ b/.github/actions/asana-get-build-variants-list/action.yml @@ -0,0 +1,36 @@ +name: Get The List of Build Variants From Asana +description: | + Fetch the lists of build variants to measure retention (ATB) and attribution (Origin) from different Asana projects and combine them in a list. +inputs: + access-token: + description: "Asana access token" + required: true + type: string + github-token: + description: "GitHub Token" + required: false + type: string + atb-asana-task-id: + description: "Asana Task id for the ATB list." + required: true + type: string + origin-asana-section-id: + description: "Asana Section id for the Origins list" + required: true + type: string +outputs: + build-variants: + description: "The list of build variants to create" + value: ${{ steps.get-build-variants-task.outputs.build-variants }} +runs: + using: "composite" + steps: + - id: get-build-variants-task + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} + ORIGIN_ASANA_SECTION_ID: ${{ inputs.origin-asana-section-id }} + ATB_ASANA_TASK_ID: ${{ inputs.atb-asana-task-id }} + run: | + ${{ github.action_path }}/get_build_variants_list.sh diff --git a/.github/actions/asana-get-build-variants-list/get_build_variants_list.sh b/.github/actions/asana-get-build-variants-list/get_build_variants_list.sh new file mode 100755 index 0000000000..a42d54ac58 --- /dev/null +++ b/.github/actions/asana-get-build-variants-list/get_build_variants_list.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# +# This scripts fetches Asana tasks from the Origins section defined in the Asana project https://app.asana.com/0/1206716555947156/1206716715679835. +# + +set -e -o pipefail + +asana_api_url="https://app.asana.com/api/1.0" + +# Create a JSON string with the `origin-variant` pairs from the list of . +_create_origins_and_variants() { + local response="$1" + local origin_field="Origin" + local atb_field="ATB" + + # for each element in the data array. + # filter out element with null `origin`. + # select `origin` and `variant` from the custom_fields response and make a key:value pair structure like {origin: , variant: }. + # if variant is not null we need to create two entries. One only with `origin` and one with `origin` and `variant` + # replace the new line with a comma + # remove the trailing comma at the end of the line. + jq -c '.data[] + | select(.custom_fields[] | select(.name == "'"${origin_field}"'").text_value != null) + | {origin: (.custom_fields[] | select(.name == "'"${origin_field}"'") | .text_value), variant: (.custom_fields[] | select(.name == "'"${atb_field}"'") | .text_value)} + | if .variant != null then {origin}, {origin, variant} else {origin} end' <<< "$response" \ + | tr '\n' ',' \ + | sed 's/,$//' +} + +# Fetch all the Asana tasks in the section specified by ORIGIN_ASANA_SECTION_ID for a project. +# This function fetches only uncompleted tasks. +# If there are more than 100 items the function takes care of pagination. +# Returns a JSON string consisting of a list of `origin-variant` pairs concatenated by a comma. Eg. `{"origin":"app","variant":"ab"},{"origin":"app.search","variant":null}`. +_fetch_origin_tasks() { + # Fetches only tasks that have not been completed yet, includes in the response section name, name of the task and its custom fields. + local query="completed_since=now&opt_fields=name,custom_fields.id_prefix,custom_fields.name,custom_fields.text_value&opt_expand=custom_fields&opt_fields=memberships.section.name&limit=100" + + local url="${asana_api_url}/sections/${ORIGIN_ASANA_SECTION_ID}/tasks?${query}" + local response + local origin_variants=() + + # go through all tasks in the section (there may be multiple requests in case there are more than 100 tasks in the section) + # repeat until no more pages (next_page.uri is null) + while true; do + response="$(curl -fLSs "$url" -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}")" + + # extract the object in the data array and append to result + origin_variants+=("$(_create_origins_and_variants "$response")") + + # set new URL to next page URL + url="$(jq -r .next_page.uri <<< "$response")" + + # break on last page + if [[ "$url" == "null" ]]; then + break + fi + done + + echo "${origin_variants}" +} + +# Create a JSON string from the list of ATB items passed. +_create_atb_variant_pairs() { + local response="$1" + + # read the response raw and format in a compact JSON mode + # map each element to the structure {variant:} + # remove the array + # replace the new line with a comma + # remove the trailing comma at the end of the line. + jq -R -c 'split(",") + | map({variant: .}) + | .[]' <<< "$response" \ + | tr '\n' ',' \ + | sed 's/,$//' +} + +# Fetches all the ATB variants defined in the ATB_ASANA_TASK_ID at the Variants list (comma separated) section. +_fetch_atb_variants() { + local url="${asana_api_url}/tasks/${ATB_ASANA_TASK_ID}?opt_fields=notes" + local atb_variants + + # fetches the items + # read the response raw + # select only Variants list section + # output last line of the input to get all the list of variants. + atb_variants="$(curl -fSsL ${url} \ + -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ + | jq -r .data.notes \ + | grep -A1 '^Variants list' \ + | tail -1)" + + variants_list=("$(_create_atb_variant_pairs "$atb_variants")") + + echo "${variants_list}" +} + +main() { + # fetch ATB variants + local atb_variants=$(_fetch_atb_variants) + # fetch Origin variants + local origin_variants=$(_fetch_origin_tasks) + # merges the two list together. Use `include` keyword for later usage in matrix. + # for more info see https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#example-adding-configurations. + local merged_variants="{\"include\": [${atb_variants},${origin_variants}]}" + # write in GitHub output + echo "build-variants=${merged_variants}" >> "$GITHUB_OUTPUT" +} + +main diff --git a/.github/workflows/create_variant.yml b/.github/workflows/create_variant.yml new file mode 100644 index 0000000000..502080b8e1 --- /dev/null +++ b/.github/workflows/create_variant.yml @@ -0,0 +1,201 @@ +on: + workflow_dispatch: + inputs: + atb-variant: + description: "ATB variant. Used for measuring attribution and retention." + required: false + type: string + origin-variant: + description: "Origin variant. Used for measuring attribution only." + required: false + type: string + workflow_call: + inputs: + atb-variant: + description: "ATB variant. Used for measuring attribution and retention. Passed by the caller workflow." + required: false + type: string + origin-variant: + description: "Origin variant. Used for measuring attribution only. Passed by the caller workflow." + required: false + type: string + secrets: + BUILD_CERTIFICATE_BASE64: + required: true + P12_PASSWORD: + required: true + KEYCHAIN_PASSWORD: + required: true + REVIEW_PROVISION_PROFILE_BASE64: + required: true + RELEASE_PROVISION_PROFILE_BASE64: + required: true + DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: + required: true + DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: + required: true + NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2: + required: true + NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2: + required: true + NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2: + required: true + NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2: + required: true + NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: + required: true + NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: + required: true + APPLE_API_KEY_BASE64: + required: true + APPLE_API_KEY_ID: + required: true + APPLE_API_KEY_ISSUER: + required: true + ASANA_ACCESS_TOKEN: + required: true + MM_HANDLES_BASE64: + required: true + MM_WEBHOOK_URL: + required: true + AWS_ACCESS_KEY_ID_RELEASE_S3: + required: true + AWS_SECRET_ACCESS_KEY_RELEASE_S3: + required: true + +jobs: + + create-dmg-variant: + + name: Create DMG Variant + + env: + ATB_VARIANT_NAME: ${{ inputs.atb-variant || github.event.inputs.atb-variant }} + ORIGIN_VARIANT_NAME: ${{ inputs.origin-variant || github.event.inputs.origin-variant }} + + runs-on: macos-12 + timeout-minutes: 15 + + steps: + + - name: Check out the code + uses: actions/checkout@v4 + with: + ref: main + sparse-checkout: | + .github + scripts + + - name: Download release app + run: | + curl -fLSs "${{ vars.RELEASE_DMG_URL }}" --output duckduckgo.dmg + hdiutil attach duckduckgo.dmg -mountpoint vanilla + mkdir -p dmg + cp -R vanilla/DuckDuckGo.app dmg/DuckDuckGo.app + hdiutil detach vanilla + rm -f duckduckgo.dmg + + - name: Install create-dmg + run: brew install create-dmg + + - name: Install Apple Developer ID Application certificate + uses: ./.github/actions/install-certs-and-profiles + with: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }} + RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }} + DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }} + DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }} + NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2 }} + NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2 }} + NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2 }} + NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2 }} + NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }} + NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }} + + - name: Set up variant + working-directory: ${{ github.workspace }}/dmg + run: | + codesign -d --entitlements :- DuckDuckGo.app > entitlements.plist + echo "${{ env.ATB_VARIANT_NAME }}" > "DuckDuckGo.app/Contents/Resources/variant.txt" + echo "${{ env.ORIGIN_VARIANT_NAME }}" > "DuckDuckGo.app/Contents/Resources/origin.txt" + sign_identity="$(security find-certificate -a -c "Developer ID Application" -Z | grep ^SHA-1 | cut -d " " -f3 | uniq)" + + /usr/bin/codesign \ + --force \ + --sign ${sign_identity} \ + --options runtime \ + --entitlements entitlements.plist \ + --generate-entitlement-der "DuckDuckGo.app" + rm -f entitlements.plist + + - name: Notarize the app + env: + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} + working-directory: ${{ github.workspace }}/dmg + run: | + # import API Key from secrets + export APPLE_API_KEY_PATH="$RUNNER_TEMP/apple_api_key.pem" + echo -n "$APPLE_API_KEY_BASE64" | base64 --decode -o $APPLE_API_KEY_PATH + + notarization_zip_path="DuckDuckGo-for-notarization.zip" + + ditto -c -k --keepParent "DuckDuckGo.app" "${notarization_zip_path}" + xcrun notarytool submit \ + --key "${APPLE_API_KEY_PATH}" \ + --key-id "${{ env.APPLE_API_KEY_ID }}" \ + --issuer "${{ env.APPLE_API_KEY_ISSUER }}" \ + --wait \ + "${notarization_zip_path}" + xcrun stapler staple "DuckDuckGo.app" + rm -rf "${notarization_zip_path}" + + - name: Create variant DMG + env: + GH_TOKEN: ${{ github.token }} + run: | + retries=3 + + while [[ $retries -gt 0 ]]; do + if create-dmg --volname "DuckDuckGo" \ + --icon "DuckDuckGo.app" 140 160 \ + --background "scripts/assets/dmg-background.png" \ + --window-size 600 400 \ + --icon-size 120 \ + --app-drop-link 430 160 "duckduckgo.dmg" \ + "dmg" + then + break + fi + retries=$((retries-1)) + done + + - name: Set variant name + id: set-variant-name + run: | + if [ -z "$ORIGIN_VARIANT_NAME" ] && [ -n "$ATB_VARIANT_NAME" ]; then + name=$ATB_VARIANT_NAME + elif [ -n "$ORIGIN_VARIANT_NAME" ] && [ -z "$ATB_VARIANT_NAME" ]; then + name=$ORIGIN_VARIANT_NAME + elif [ -n "$ORIGIN_VARIANT_NAME" ] && [ -n "$ATB_VARIANT_NAME" ]; then + name="${ORIGIN_VARIANT_NAME}-${ATB_VARIANT_NAME}" + else + echo "Neither ATB_VARIANT_NAME nor ORIGIN_VARIANT_NAME is set" + exit 1 + fi + + echo "variant-name=${name}" >> "$GITHUB_OUTPUT" + + - name: Upload variant DMG + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + run: | + aws s3 cp duckduckgo.dmg \ + s3://${{ vars.RELEASE_BUCKET_NAME }}/${{ vars.RELEASE_BUCKET_PREFIX }}/${{ steps.set-variant-name.outputs.variant-name }}/duckduckgo.dmg \ + --acl public-read diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml index 734d71d8ff..ccddcecbb2 100644 --- a/.github/workflows/create_variants.yml +++ b/.github/workflows/create_variants.yml @@ -2,11 +2,7 @@ name: Create DMG Variants on: workflow_dispatch: - inputs: - atb-variants: - description: "ATB variants (comma-separated)" - required: true - type: string + workflow_call: secrets: BUILD_CERTIFICATE_BASE64: @@ -62,164 +58,66 @@ jobs: timeout-minutes: 15 outputs: - atb-variants: ${{ steps.atb-variants.outputs.matrix }} + build-variants: ${{ steps.get-build-variants.outputs.build-variants }} steps: - - - name: Set up ATB variants - id: atb-variants - env: - ASANA_TASK_ID: ${{ vars.DMG_VARIANTS_LIST_TASK_ID }} - ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} - run: | - atb_variants="${{ github.event.inputs.atb-variants }}" - if [[ -z "${atb_variants}" ]]; then - atb_variants="$(curl -fSsL "https://app.asana.com/api/1.0/tasks/${ASANA_TASK_ID}?opt_fields=notes" \ - -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \ - | jq -r .data.notes \ - | grep -A1 '^Variants list' \ - | tail -1)" - fi - echo "atb-variants=${atb_variants}" >> $GITHUB_ENV - variant_matrix="$(sed 's/,/\",\"/g' <<< "${atb_variants}")" - echo "matrix={\"variant\": [\"${variant_matrix}\"]}" >> $GITHUB_OUTPUT - - create-atb-variants: - - name: Create ATB Variant + - name: Check out repository + uses: actions/checkout@v4 + + - name: Fetch Build Variants + id: get-build-variants + uses: ./.github/actions/asana-get-build-variants-list + with: + access-token: ${{ secrets.ASANA_ACCESS_TOKEN }} + atb-asana-task-id: ${{ vars.DMG_VARIANTS_LIST_TASK_ID }} + origin-asana-section-id: ${{ vars.DMG_VARIANTS_ORIGIN_SECTION_ID }} + + + create-variants: + + name: Create Variant needs: set-up-variants strategy: fail-fast: false - matrix: ${{ fromJSON(needs.set-up-variants.outputs.atb-variants) }} - - runs-on: macos-12 - timeout-minutes: 15 - - steps: - - - name: Download release app - run: | - curl -fLSs "${{ vars.RELEASE_DMG_URL }}" --output duckduckgo.dmg - hdiutil attach duckduckgo.dmg -mountpoint vanilla - mkdir -p dmg - cp -R vanilla/DuckDuckGo.app dmg/DuckDuckGo.app - hdiutil detach vanilla - rm -f duckduckgo.dmg - - - name: Install create-dmg - run: brew install create-dmg - - - name: Fetch install-certs-and-profiles action - env: - GH_TOKEN: ${{ github.token }} - DEST_DIR: ".github/actions/install-certs-and-profiles" - run: | - mkdir -p "${{ env.DEST_DIR }}" - curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/${{ env.DEST_DIR }}/action.yml?ref=${{ github.ref }} --jq .download_url) \ - --output ${{ env.DEST_DIR }}/action.yml - - - name: Install Apple Developer ID Application certificate - uses: ./.github/actions/install-certs-and-profiles - with: - BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} - P12_PASSWORD: ${{ secrets.P12_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }} - RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }} - DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }} - DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }} - NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2 }} - NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2 }} - NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2 }} - NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2 }} - NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }} - NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }} - - - name: Set up variant - working-directory: ${{ github.workspace }}/dmg - run: | - codesign -d --entitlements :- DuckDuckGo.app > entitlements.plist - echo "${{ matrix.variant }}" > "DuckDuckGo.app/Contents/Resources/variant.txt" - sign_identity="$(security find-certificate -a -c "Developer ID Application" -Z | grep ^SHA-1 | cut -d " " -f3 | uniq)" - - /usr/bin/codesign \ - --force \ - --sign ${sign_identity} \ - --options runtime \ - --entitlements entitlements.plist \ - --generate-entitlement-der "DuckDuckGo.app" - rm -f entitlements.plist - - - name: Notarize the app - env: - APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} - APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} - working-directory: ${{ github.workspace }}/dmg - run: | - # import API Key from secrets - export APPLE_API_KEY_PATH="$RUNNER_TEMP/apple_api_key.pem" - echo -n "$APPLE_API_KEY_BASE64" | base64 --decode -o $APPLE_API_KEY_PATH - - notarization_zip_path="DuckDuckGo-for-notarization.zip" - - ditto -c -k --keepParent "DuckDuckGo.app" "${notarization_zip_path}" - xcrun notarytool submit \ - --key "${APPLE_API_KEY_PATH}" \ - --key-id "${{ env.APPLE_API_KEY_ID }}" \ - --issuer "${{ env.APPLE_API_KEY_ISSUER }}" \ - --wait \ - "${notarization_zip_path}" - xcrun stapler staple "DuckDuckGo.app" - rm -rf "${notarization_zip_path}" - - - name: Create variant DMG - env: - GH_TOKEN: ${{ github.token }} - run: | - curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/dmg-background.png?ref=${{ github.ref }} --jq .download_url) \ - --output dmg-background.png - - retries=3 - - while [[ $retries -gt 0 ]]; do - if create-dmg --volname "DuckDuckGo" \ - --icon "DuckDuckGo.app" 140 160 \ - --background "dmg-background.png" \ - --window-size 600 400 \ - --icon-size 120 \ - --app-drop-link 430 160 "duckduckgo.dmg" \ - "dmg" - then - break - fi - retries=$((retries-1)) - done - - - - name: Upload variant DMG - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} - AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} - run: | - aws s3 cp duckduckgo.dmg \ - s3://${{ vars.RELEASE_BUCKET_NAME }}/${{ vars.RELEASE_BUCKET_PREFIX }}/${{ matrix.variant }}/duckduckgo.dmg \ - --acl public-read - + matrix: ${{ fromJSON(needs.set-up-variants.outputs.build-variants) }} + uses: ./.github/workflows/create_variant.yml + with: + atb-variant: ${{ matrix.variant }} + origin-variant: ${{ matrix.origin }} + secrets: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }} + RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }} + DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }} + DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }} + NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2 }} + NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2 }} + NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2 }} + NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2 }} + NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }} + NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }} + APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }} + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + MM_HANDLES_BASE64: ${{ secrets.MM_HANDLES_BASE64 }} + MM_WEBHOOK_URL: ${{ secrets.MM_WEBHOOK_URL }} + AWS_ACCESS_KEY_ID_RELEASE_S3: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }} + AWS_SECRET_ACCESS_KEY_RELEASE_S3: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }} + mattermost: - + name: Send Mattermost message - - needs: create-atb-variants - + needs: create-variants runs-on: ubuntu-latest - + env: - success: ${{ needs.create-atb-variants.result == 'success' }} - failure: ${{ needs.create-atb-variants.result == 'failure' }} - + success: ${{ needs.create-variants.result == 'success' }} + failure: ${{ needs.create-variants.result == 'failure' }} + steps: - name: Send Mattermost message if: ${{ env.success || env.failure }} # Don't execute when cancelled @@ -229,13 +127,13 @@ jobs: run: | curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/variants-release-mm-template.json?ref=${{ github.ref }} --jq .download_url) \ --output message-template.json - + export MM_USER_HANDLE=$(base64 -d <<< ${{ secrets.MM_HANDLES_BASE64 }} | jq ".${{ github.actor }}" | tr -d '"') - + if [[ -z "${MM_USER_HANDLE}" ]]; then echo "Mattermost user handle not known for ${{ github.actor }}, skipping sending message" else - + if [[ "${{ env.success }}" == "true" ]]; then status="success" else diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9d9985b44c..3269bcf0a5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2527,6 +2527,24 @@ 9FA173ED2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */; }; 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; 9FA75A3F2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */; }; + 9FBD84522BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; + 9FBD84532BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; + 9FBD84542BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */; }; + 9FBD84562BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */; }; + 9FBD84572BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */; }; + 9FBD845D2BB3B80300220859 /* Origin.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9FBD845C2BB3B80300220859 /* Origin.txt */; }; + 9FBD845E2BB3B80300220859 /* Origin.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9FBD845C2BB3B80300220859 /* Origin.txt */; }; + 9FBD84612BB3BC6400220859 /* Origin-empty.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9FBD84602BB3BC6400220859 /* Origin-empty.txt */; }; + 9FBD84622BB3BC6400220859 /* Origin-empty.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9FBD84602BB3BC6400220859 /* Origin-empty.txt */; }; + 9FBD84702BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD846F2BB3DD8400220859 /* MockAttributionsPixelHandler.swift */; }; + 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD846F2BB3DD8400220859 /* MockAttributionsPixelHandler.swift */; }; + 9FBD84732BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84722BB3E15D00220859 /* InstallationAttributionPixelHandler.swift */; }; + 9FBD84742BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84722BB3E15D00220859 /* InstallationAttributionPixelHandler.swift */; }; + 9FBD84752BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84722BB3E15D00220859 /* InstallationAttributionPixelHandler.swift */; }; + 9FBD84772BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84762BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift */; }; + 9FBD84782BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84762BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift */; }; + 9FBD847A2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84792BB3EC3300220859 /* MockAttributionOriginProvider.swift */; }; + 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84792BB3EC3300220859 /* MockAttributionOriginProvider.swift */; }; 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; @@ -4224,6 +4242,14 @@ 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; 9FA75A3D2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactoryTests.swift; sourceTree = ""; }; + 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionOriginFileProvider.swift; sourceTree = ""; }; + 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionOriginFileProviderTests.swift; sourceTree = ""; }; + 9FBD845C2BB3B80300220859 /* Origin.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = Origin.txt; sourceTree = ""; }; + 9FBD84602BB3BC6400220859 /* Origin-empty.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Origin-empty.txt"; sourceTree = ""; }; + 9FBD846F2BB3DD8400220859 /* MockAttributionsPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAttributionsPixelHandler.swift; sourceTree = ""; }; + 9FBD84722BB3E15D00220859 /* InstallationAttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationAttributionPixelHandler.swift; sourceTree = ""; }; + 9FBD84762BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationAttributionPixelHandlerTests.swift; sourceTree = ""; }; + 9FBD84792BB3EC3300220859 /* MockAttributionOriginProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAttributionOriginProvider.swift; sourceTree = ""; }; 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = ""; }; 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = ""; }; @@ -8453,6 +8479,8 @@ B69B50392726A12500758A2B /* LocalStatisticsStore.swift */, B69B50372726A12000758A2B /* VariantManager.swift */, B69B50562727D16900758A2B /* AtbAndVariantCleanup.swift */, + 9FBD84512BB3AACB00220859 /* AttributionOriginFileProvider.swift */, + 9FBD84722BB3E15D00220859 /* InstallationAttributionPixelHandler.swift */, ); path = ATB; sourceTree = ""; @@ -8466,6 +8494,8 @@ B69B50442726C5C200758A2B /* StatisticsLoaderTests.swift */, B69B50432726C5C100758A2B /* VariantManagerTests.swift */, 857E44612A9F6F3500ED77A7 /* CampaignVariantTests.swift */, + 9FBD84552BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift */, + 9FBD84762BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift */, ); path = ATB; sourceTree = ""; @@ -8479,6 +8509,10 @@ B69B504E2726CD7E00758A2B /* atb.json */, B69B504F2726CD7F00758A2B /* empty */, B69B50512726CD8000758A2B /* invalid.json */, + 9FBD845C2BB3B80300220859 /* Origin.txt */, + 9FBD84602BB3BC6400220859 /* Origin-empty.txt */, + 9FBD846F2BB3DD8400220859 /* MockAttributionsPixelHandler.swift */, + 9FBD84792BB3EC3300220859 /* MockAttributionOriginProvider.swift */, ); path = Mock; sourceTree = ""; @@ -9588,8 +9622,10 @@ 3706FE8B293F661700E42796 /* empty in Resources */, 376E2D282942843D001CD31B /* privacy-reference-tests in Resources */, 3706FE8C293F661700E42796 /* atb-with-update.json in Resources */, + 9FBD84622BB3BC6400220859 /* Origin-empty.txt in Resources */, 3706FE8D293F661700E42796 /* DataImportResources in Resources */, 3706FE8E293F661700E42796 /* atb.json in Resources */, + 9FBD845E2BB3B80300220859 /* Origin.txt in Resources */, 3706FE8F293F661700E42796 /* DuckDuckGo-ExampleCrash.ips in Resources */, 3706FE90293F661700E42796 /* DuckDuckGo-Symbol.jpg in Resources */, 3706FE91293F661700E42796 /* invalid.json in Resources */, @@ -9827,6 +9863,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9FBD845D2BB3B80300220859 /* Origin.txt in Resources */, B69B50532726CD8100758A2B /* empty in Resources */, 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */, B69B50542726CD8100758A2B /* atb-with-update.json in Resources */, @@ -9838,6 +9875,7 @@ 4BCF15ED2ABB9B180083F6DF /* network-protection-messages.json in Resources */, B67C6C422654BF49006C872E /* DuckDuckGo-Symbol.jpg in Resources */, B69B50552726CD8100758A2B /* invalid.json in Resources */, + 9FBD84612BB3BC6400220859 /* Origin-empty.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10366,6 +10404,7 @@ 3706FAF7293F65D500E42796 /* FireproofDomainsViewController.swift in Sources */, 4BF0E5062AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 3706FAF8293F65D500E42796 /* URLEventHandler.swift in Sources */, + 9FBD84742BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */, 37197EA72942443D00394917 /* AuthenticationAlert.swift in Sources */, 3706FEC3293F6F0600E42796 /* BWCommunicator.swift in Sources */, 3706FAFA293F65D500E42796 /* CleanThisHistoryMenuItem.swift in Sources */, @@ -10612,6 +10651,7 @@ B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, + 9FBD84532BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */, 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, @@ -11009,6 +11049,7 @@ 3706FDF9293F661700E42796 /* TabViewModelTests.swift in Sources */, 9F872DA12B90644800138637 /* ContextualMenuTests.swift in Sources */, 3706FDFA293F661700E42796 /* DefaultBrowserPreferencesTests.swift in Sources */, + 9FBD84782BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift in Sources */, 3706FDFB293F661700E42796 /* DispatchQueueExtensionsTests.swift in Sources */, 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */, 981E20B6299A39B8002B68CD /* BookmarkMigrationTests.swift in Sources */, @@ -11030,6 +11071,7 @@ 3706FE09293F661700E42796 /* VariantManagerTests.swift in Sources */, 3706FE0A293F661700E42796 /* UserAgentTests.swift in Sources */, 3706FE0B293F661700E42796 /* AVCaptureDeviceMock.swift in Sources */, + 9FBD84572BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */, 3706FE0C293F661700E42796 /* GeolocationProviderMock.swift in Sources */, CBDD5DE429A6800300832877 /* MockConfigurationStore.swift in Sources */, 3706FE0D293F661700E42796 /* MainMenuTests.swift in Sources */, @@ -11216,7 +11258,9 @@ 3706FE81293F661700E42796 /* PermissionModelTests.swift in Sources */, B60C6F8529B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, 567DA94629E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */, + 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, 3706FE82293F661700E42796 /* MockStatisticsStore.swift in Sources */, + 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, 3706FE83293F661700E42796 /* AutofillPreferencesModelTests.swift in Sources */, 3706FE84293F661700E42796 /* TabCollectionViewModelTests+PinnedTabs.swift in Sources */, B603975229C1FFAD00902A34 /* ExpectedNavigationExtension.swift in Sources */, @@ -11705,6 +11749,7 @@ 4B957A2D2AC7AE700062CA31 /* NSPopUpButtonView.swift in Sources */, 4B957A2E2AC7AE700062CA31 /* BlockMenuItem.swift in Sources */, 4B957A2F2AC7AE700062CA31 /* ContextualMenu.swift in Sources */, + 9FBD84752BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */, 4B957A302AC7AE700062CA31 /* NavigationBarViewController.swift in Sources */, 4B957A312AC7AE700062CA31 /* MainViewController.swift in Sources */, 4B957A322AC7AE700062CA31 /* DuckPlayer.swift in Sources */, @@ -11880,6 +11925,7 @@ 4B957AC12AC7AE700062CA31 /* SavePaymentMethodPopover.swift in Sources */, 4B957AC22AC7AE700062CA31 /* FindInPageViewController.swift in Sources */, 4B957AC32AC7AE700062CA31 /* Cryptography.swift in Sources */, + 9FBD84542BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */, 4B957AC42AC7AE700062CA31 /* BWVault.swift in Sources */, 4B957AC52AC7AE700062CA31 /* NSViewExtension.swift in Sources */, BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */, @@ -12333,6 +12379,7 @@ 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */, 1D01A3D82B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */, + 9FBD84522BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */, 1DC669702B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, AAC30A26268DFEE200D2D9CD /* CrashReporter.swift in Sources */, B60D64492AAF1B7C00B26F50 /* AddressBarTextSelectionNavigation.swift in Sources */, @@ -12796,6 +12843,7 @@ AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */, 4BB99D0126FE191E001E4761 /* ChromiumBookmarksReader.swift in Sources */, B6C0B23426E71BCD0031CB7F /* Downloads.xcdatamodeld in Sources */, + 9FBD84732BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */, AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */, B6C8CAA72AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */, @@ -13164,6 +13212,7 @@ 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 373A1AAA283ED86C00586521 /* BookmarksHTMLReaderTests.swift in Sources */, 317295D42AF058D3002C3206 /* MockWaitlistFeatureSetupHandler.swift in Sources */, + 9FBD84772BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift in Sources */, AA9C363025518CA9004B1BA3 /* FireTests.swift in Sources */, B6106BB126A7D8720013B453 /* PermissionStoreTests.swift in Sources */, 4BF4951826C08395000547B8 /* ThirdPartyBrowserTests.swift in Sources */, @@ -13239,6 +13288,7 @@ B610F2EB27AA8E4500FCEBE9 /* ContentBlockingUpdatingTests.swift in Sources */, 4BA1A6F6258C4F9600F6F690 /* EncryptionMocks.swift in Sources */, 3767190028E58513003A2A15 /* DuckPlayerURLExtensionTests.swift in Sources */, + 9FBD847A2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, 4B2975992828285900187C4E /* FirefoxKeyReaderTests.swift in Sources */, B698E5042908011E00A746A8 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, 56D145EE29E6DAD900E3488A /* DataImportProviderTests.swift in Sources */, @@ -13247,6 +13297,7 @@ 1D9FDEB72B9B5D150040B78C /* SearchPreferencesTests.swift in Sources */, 1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */, 4BB99D1026FE1A84001E4761 /* FirefoxBookmarksReaderTests.swift in Sources */, + 9FBD84702BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, 4B117F7D276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift in Sources */, AAEC74B42642C69300C2EFBC /* HistoryCoordinatorTests.swift in Sources */, 378205F62837CBA800D1D4AA /* SavedStateMock.swift in Sources */, @@ -13257,6 +13308,7 @@ AA9C362825518C44004B1BA3 /* WebsiteDataStoreMock.swift in Sources */, 857E44652A9F70F300ED77A7 /* CampaignVariantTests.swift in Sources */, 3776582D27F71652009A6B35 /* WebsiteBreakageReportTests.swift in Sources */, + 9FBD84562BB3ACFD00220859 /* AttributionOriginFileProviderTests.swift in Sources */, 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */, 1D9FDEC32B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 4B723E0826B0003E00E14D75 /* MockSecureVault.swift in Sources */, diff --git a/DuckDuckGo/Statistics/ATB/AttributionOriginFileProvider.swift b/DuckDuckGo/Statistics/ATB/AttributionOriginFileProvider.swift new file mode 100644 index 0000000000..53a223e3e5 --- /dev/null +++ b/DuckDuckGo/Statistics/ATB/AttributionOriginFileProvider.swift @@ -0,0 +1,47 @@ +// +// AttributionOriginFileProvider.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 + +/// A type that provides the `origin` used to track installations without tracking retention. +protocol AttributionOriginProvider: AnyObject { + /// A string representing the acquisition funnel. + var origin: String? { get } +} + +final class AttributionOriginFileProvider: AttributionOriginProvider { + let origin: String? + + /// Creates an instance with the given file name and `Bundle`. + /// - Parameters: + /// - name: The name of the Txt file to extract the origin from. + /// - bundle: The bundle where the file is located. In tests pass replace this with the test bundle. + init(resourceName name: String = "Origin", bundle: Bundle = .main) { + let url = bundle.url(forResource: name, withExtension: "txt") + origin = try? url + .flatMap(String.init(contentsOf:))? + .trimmingCharacters(in: .whitespacesAndNewlines) + .nilIfEmpty + } +} + +private extension String { + var nilIfEmpty: String? { + return isEmpty ? nil : self + } +} diff --git a/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift new file mode 100644 index 0000000000..9a0baf2bff --- /dev/null +++ b/DuckDuckGo/Statistics/ATB/InstallationAttributionPixelHandler.swift @@ -0,0 +1,87 @@ +// +// InstallationAttributionPixelHandler.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 + +/// A type that handles Pixels for acquisition attributions. +protocol AttributionsPixelHandler: AnyObject { + /// Fire the Pixel to track the App install. + func fireInstallationAttributionPixel() +} + +final class InstallationAttributionPixelHandler: AttributionsPixelHandler { + enum Parameters { + static let origin = "origin" + static let locale = "locale" + } + + private let fireRequest: FireRequest + private let originProvider: AttributionOriginProvider + private let locale: Locale + + /// Creates an instance with the specified fire request, origin provider and locale. + /// - Parameters: + /// - fireRequest: A function for sending the Pixel request. + /// - originProvider: A provider for the origin used to track the acquisition funnel. + /// - locale: The locale of the device. + init( + fireRequest: @escaping FireRequest = Pixel.fire, + originProvider: AttributionOriginProvider = AttributionOriginFileProvider(), + locale: Locale = .current + ) { + self.fireRequest = fireRequest + self.originProvider = originProvider + self.locale = locale + } + + func fireInstallationAttributionPixel() { + fireRequest( + .installationAttribution, + .initial, + additionalParameters(origin: originProvider.origin, locale: locale.identifier), + nil, + true, + { _ in } + ) + } +} + +// MARK: - Parameter + +private extension InstallationAttributionPixelHandler { + func additionalParameters(origin: String?, locale: String) -> [String: String] { + var dictionary = [Self.Parameters.locale: locale] + if let origin { + dictionary[Self.Parameters.origin] = origin + } + return dictionary + } +} + +// MARK: - FireRequest + +extension InstallationAttributionPixelHandler { + typealias FireRequest = ( + _ event: Pixel.Event, + _ repetition: Pixel.Event.Repetition, + _ parameters: [String: String]?, + _ allowedQueryReservedCharacters: CharacterSet?, + _ includeAppVersionParameter: Bool, + _ onComplete: @escaping (Error?) -> Void + ) -> Void +} diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 296f67e901..72a928c43b 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -29,12 +29,18 @@ final class StatisticsLoader { private let statisticsStore: StatisticsStore private let emailManager: EmailManager + private let attributionPixelHandler: AttributionsPixelHandler private let parser = AtbParser() private var isAppRetentionRequestInProgress = false - init(statisticsStore: StatisticsStore = LocalStatisticsStore(), emailManager: EmailManager = EmailManager()) { + init( + statisticsStore: StatisticsStore = LocalStatisticsStore(), + emailManager: EmailManager = EmailManager(), + attributionPixelHandler: AttributionsPixelHandler = InstallationAttributionPixelHandler() + ) { self.statisticsStore = statisticsStore self.emailManager = emailManager + self.attributionPixelHandler = attributionPixelHandler } func refreshRetentionAtb(isSearch: Bool, completion: @escaping Completion = {}) { @@ -94,6 +100,7 @@ final class StatisticsLoader { if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.requestExti(atb: atb, completion: completion) + self.attributionPixelHandler.fireInstallationAttributionPixel() } else { completion() } diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 96b9540699..7bd1a66adc 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -277,6 +277,9 @@ extension Pixel { case passwordImportKeychainPrompt case passwordImportKeychainPromptDenied + // Tracks installation without tracking retention. + case installationAttribution + enum Debug { /// This is a convenience pixel that allows us to fire `PixelKitEvents` using our /// regular `Pixel.fire()` calls. This is a convenience intermediate step to help ensure @@ -713,6 +716,9 @@ extension Pixel.Event { // Password Import Keychain Prompt case .passwordImportKeychainPrompt: return "m_mac_password_import_keychain_prompt" case .passwordImportKeychainPromptDenied: return "m_mac_password_import_keychain_prompt_denied" + + // Installation Attribution + case .installationAttribution: return "m_mac_install" } } } diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index d6f9496add..92f0ae69e8 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -215,7 +215,8 @@ extension Pixel.Event { .privacyProWelcomeFAQClick, .privacyProPurchaseStripeSuccess, .passwordImportKeychainPrompt, - .passwordImportKeychainPromptDenied: + .passwordImportKeychainPromptDenied, + .installationAttribution: return nil } } diff --git a/UnitTests/Statistics/ATB/AttributionOriginFileProviderTests.swift b/UnitTests/Statistics/ATB/AttributionOriginFileProviderTests.swift new file mode 100644 index 0000000000..e90408c532 --- /dev/null +++ b/UnitTests/Statistics/ATB/AttributionOriginFileProviderTests.swift @@ -0,0 +1,61 @@ +// +// AttributionOriginFileProviderTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class AttributionOriginFileProviderTests: XCTestCase { + private var sut: AttributionOriginFileProvider! + + func testWhenFileAndValueExistThenReturnOriginValue() { + // GIVEN + sut = AttributionOriginFileProvider(bundle: .test) + + // WHEN + let result = sut.origin + + // THEN + XCTAssertEqual(result, "app_search") + } + + func testWhenFileDoesNotExistThenReturnNil() { + // GIVEN + sut = AttributionOriginFileProvider(resourceName: #function, bundle: .test) + + // WHEN + let result = sut.origin + + // THEN + XCTAssertNil(result) + } + + func testWhenFileExistAndIsEmptyThenReturnNil() { + // GIVEN + sut = AttributionOriginFileProvider(resourceName: "Origin-empty", bundle: .test) + + // WHEN + let result = sut.origin + + // THEN + XCTAssertNil(result) + } +} + +private extension Bundle { + static let test = Bundle(for: AttributionOriginFileProviderTests.self) +} diff --git a/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift new file mode 100644 index 0000000000..f94f1f402f --- /dev/null +++ b/UnitTests/Statistics/ATB/InstallationAttributionPixelHandlerTests.swift @@ -0,0 +1,124 @@ +// +// InstallationAttributionPixelHandlerTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class InstallationAttributionPixelHandlerTests: XCTestCase { + private var sut: InstallationAttributionPixelHandler! + private var capturedParams: CapturedParameters! + private var fireRequest: InstallationAttributionPixelHandler.FireRequest! + + override func setUpWithError() throws { + try super.setUpWithError() + capturedParams = CapturedParameters() + fireRequest = { event, repetition, parameters, reservedCharacters, includeAppVersion, onComplete in + self.capturedParams.event = event + self.capturedParams.repetition = repetition + self.capturedParams.parameters = parameters + self.capturedParams.reservedCharacters = reservedCharacters + self.capturedParams.includeAppVersion = includeAppVersion + self.capturedParams.onComplete = onComplete + } + } + + override func tearDownWithError() throws { + capturedParams = nil + fireRequest = nil + sut = nil + try super.tearDownWithError() + } + + func testWhenPixelFiresThenNameIsSetToM_Mac_Install() { + // GIVEN + sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + + // WHEN + sut.fireInstallationAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.event?.name, "m_mac_install") + } + + func testWhenPixelFiresThenLanguageCodeIsSet() { + // GIVEN + let locale = Locale(identifier: "hu-HU") + sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: locale) + + // WHEN + sut.fireInstallationAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "hu-HU") + } + + func testWhenPixelFiresAndOriginIsNotNilThenOriginIsSet() { + // GIVEN + let origin = "app_search" + let locale = Locale(identifier: "en-US") + let originProvider = MockAttributionOriginProvider(origin: origin) + sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) + + // WHEN + sut.fireInstallationAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin], origin) + XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresAndOriginIsNilThenOnlyLocaleIsSet() { + // GIVEN + let origin: String? = nil + let locale = Locale(identifier: "en-US") + let originProvider = MockAttributionOriginProvider(origin: origin) + sut = .init(fireRequest: fireRequest, originProvider: originProvider, locale: locale) + + // WHEN + sut.fireInstallationAttributionPixel() + + // THEN + XCTAssertNil(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.origin]) + XCTAssertEqual(capturedParams?.parameters?[InstallationAttributionPixelHandler.Parameters.locale], "en-US") + } + + func testWhenPixelFiresThenAddAppVersionIsTrueAndRepetitionIsInitial() { + // GIVEN + sut = .init(fireRequest: fireRequest, originProvider: MockAttributionOriginProvider(), locale: .current) + + // WHEN + sut.fireInstallationAttributionPixel() + + // THEN + XCTAssertEqual(capturedParams.includeAppVersion, true) + XCTAssertEqual(capturedParams.repetition, .initial) + } +} + +extension InstallationAttributionPixelHandlerTests { + + struct CapturedParameters { + var event: Pixel.Event? + var repetition: Pixel.Event.Repetition = .repetitive + var parameters: [String: String]? + var reservedCharacters: CharacterSet? + var includeAppVersion: Bool? + var onComplete: (Error?) -> Void = { _ in } + } + +} diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift new file mode 100644 index 0000000000..05e91bff49 --- /dev/null +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionOriginProvider.swift @@ -0,0 +1,28 @@ +// +// MockAttributionOriginProvider.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 +@testable import DuckDuckGo_Privacy_Browser + +final class MockAttributionOriginProvider: AttributionOriginProvider { + let origin: String? + + init(origin: String? = nil) { + self.origin = origin + } +} diff --git a/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift new file mode 100644 index 0000000000..a32ee60a30 --- /dev/null +++ b/UnitTests/Statistics/ATB/Mock/MockAttributionsPixelHandler.swift @@ -0,0 +1,30 @@ +// +// MockAttributionsPixelHandler.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 +@testable import DuckDuckGo_Privacy_Browser + +final class MockAttributionsPixelHandler: AttributionsPixelHandler { + private(set) var fireInstallationAttributionPixelCount = 0 + private(set) var didCallFireInstallationAttributionPixel = false + + func fireInstallationAttributionPixel() { + fireInstallationAttributionPixelCount += 1 + didCallFireInstallationAttributionPixel = true + } +} diff --git a/UnitTests/Statistics/ATB/Mock/Origin-empty.txt b/UnitTests/Statistics/ATB/Mock/Origin-empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/UnitTests/Statistics/ATB/Mock/Origin.txt b/UnitTests/Statistics/ATB/Mock/Origin.txt new file mode 100644 index 0000000000..2598289de9 --- /dev/null +++ b/UnitTests/Statistics/ATB/Mock/Origin.txt @@ -0,0 +1 @@ +app_search diff --git a/UnitTests/Statistics/ATB/StatisticsLoaderTests.swift b/UnitTests/Statistics/ATB/StatisticsLoaderTests.swift index bb572ecdf8..573284cd17 100644 --- a/UnitTests/Statistics/ATB/StatisticsLoaderTests.swift +++ b/UnitTests/Statistics/ATB/StatisticsLoaderTests.swift @@ -23,16 +23,21 @@ import OHHTTPStubsSwift class StatisticsLoaderTests: XCTestCase { - var mockStatisticsStore: StatisticsStore! - var testee: StatisticsLoader! + private var mockAttributionsPixelHandler: MockAttributionsPixelHandler! + private var mockStatisticsStore: StatisticsStore! + private var testee: StatisticsLoader! override func setUp() { + mockAttributionsPixelHandler = MockAttributionsPixelHandler() mockStatisticsStore = MockStatisticsStore() - testee = StatisticsLoader(statisticsStore: mockStatisticsStore) + testee = StatisticsLoader(statisticsStore: mockStatisticsStore, attributionPixelHandler: mockAttributionsPixelHandler) } override func tearDown() { HTTPStubs.removeAllStubs() + mockStatisticsStore = nil + mockAttributionsPixelHandler = nil + testee = nil super.tearDown() } @@ -296,6 +301,55 @@ class StatisticsLoaderTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } + func testWhenLoadHasSuccessfulAtbThenAttributionPixelShouldFire() { + // GIVEN + loadSuccessfulAtbStub() + let expect = expectation(description: #function) + XCTAssertFalse(mockAttributionsPixelHandler.didCallFireInstallationAttributionPixel) + + // WHEN + testee.load { + expect.fulfill() + } + + // THEN + waitForExpectations(timeout: 1, handler: nil) + XCTAssertTrue(mockAttributionsPixelHandler.didCallFireInstallationAttributionPixel) + } + + func testWhenLoadHasUnsuccessfulAtbThenAttributionPixelShouldNotFire() { + // GIVEN + loadUnsuccessfulAtbStub() + let expect = expectation(description: #function) + XCTAssertFalse(mockAttributionsPixelHandler.didCallFireInstallationAttributionPixel) + + testee.load { + expect.fulfill() + } + + // THEN + waitForExpectations(timeout: 1, handler: nil) + XCTAssertFalse(mockAttributionsPixelHandler.didCallFireInstallationAttributionPixel) + } + + func testWhenLoadHasSuccessfulAtbSubsequentlyThenAttributionPixelShouldNotFire() { + // GIVEN + loadSuccessfulAtbStub() + let firstATBCallExpectation = XCTestExpectation(description: "First ATB call") + let secondATBCallExpectation = XCTestExpectation(description: "Second ATB call") + testee.load { firstATBCallExpectation.fulfill() } + wait(for: [firstATBCallExpectation], timeout: 1.0) + XCTAssertTrue(mockAttributionsPixelHandler.didCallFireInstallationAttributionPixel) + XCTAssertEqual(mockAttributionsPixelHandler.fireInstallationAttributionPixelCount, 1) + + // WHEN + testee.load { secondATBCallExpectation.fulfill() } + + // THEN + wait(for: [secondATBCallExpectation], timeout: 1.0) + XCTAssertEqual(mockAttributionsPixelHandler.fireInstallationAttributionPixelCount, 1) + } + func loadSuccessfulAtbStub() { stub(condition: isHost(URL.initialAtb.host!)) { _ in let path = OHPathForFile("atb.json", type(of: self))! From 22fbc83983c44f33c2196885694d7b942f2dcbe4 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 5 Apr 2024 18:02:09 +0500 Subject: [PATCH 016/221] fix download save panel disappearing on navigation (#2549) Task/Issue URL: https://app.asana.com/0/0/1206895139412667/f --- DuckDuckGo/Tab/Model/Tab+Dialogs.swift | 30 +++++++++++++++++-- .../Tab/View/BrowserTabViewController.swift | 10 +++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/Model/Tab+Dialogs.swift b/DuckDuckGo/Tab/Model/Tab+Dialogs.swift index 6a79217db6..851e103161 100644 --- a/DuckDuckGo/Tab/Model/Tab+Dialogs.swift +++ b/DuckDuckGo/Tab/Model/Tab+Dialogs.swift @@ -40,7 +40,7 @@ typealias AlertDialogRequest = UserDialogRequest typealias BasicAuthDialogRequest = UserDialogRequest typealias PrintDialogRequest = UserDialogRequest -enum JSAlertQuery { +enum JSAlertQuery: Equatable { case confirm(ConfirmDialogRequest) case textInput(TextInputDialogRequest) case alert(AlertDialogRequest) @@ -55,16 +55,35 @@ enum JSAlertQuery { return request.submit(nil) } } + + static func == (lhs: JSAlertQuery, rhs: JSAlertQuery) -> Bool { + switch lhs { + case .confirm(let r1): if case .confirm(let r2) = rhs { r1 === r2 } else { false } + case .textInput(let r1): if case .textInput(let r2) = rhs { r1 === r2 } else { false } + case .alert(let r1): if case .alert(let r2) = rhs { r1 === r2 } else { false } + } + } } extension Tab { - enum UserDialogType { + enum UserDialogType: Equatable { case openPanel(OpenPanelDialogRequest) case savePanel(SavePanelDialogRequest) case jsDialog(JSAlertQuery) case basicAuthenticationChallenge(BasicAuthDialogRequest) case print(PrintDialogRequest) + + static func == (lhs: Tab.UserDialogType, rhs: Tab.UserDialogType) -> Bool { + switch lhs { + case .openPanel(let r1): if case .openPanel(let r2) = rhs { r1 === r2 } else { false } + case .savePanel(let r1): if case .savePanel(let r2) = rhs { r1 === r2 } else { false } + case .jsDialog(let r1): if case .jsDialog(let r2) = rhs { r1 == r2 } else { false } + case .basicAuthenticationChallenge(let r1): if case .basicAuthenticationChallenge(let r2) = rhs { r1 === r2 } else { false } + case .print(let r1): if case .print(let r2) = rhs { r1 === r2 } else { false } + } + } + } enum UserDialogSender: Equatable { @@ -72,7 +91,7 @@ extension Tab { case page(domain: String) } - struct UserDialog { + struct UserDialog: Equatable { let sender: UserDialogSender let dialog: UserDialogType @@ -87,6 +106,11 @@ extension Tab { case .print(let request): return request } } + + static func == (lhs: Tab.UserDialog, rhs: Tab.UserDialog) -> Bool { + lhs.sender == rhs.sender && rhs.dialog == rhs.dialog + } + } } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index d3e4ebbc31..539d7007ea 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -425,6 +425,7 @@ final class BrowserTabViewController: NSViewController { tabViewModel.tab.downloads?.savePanelDialogPublisher ?? Just(nil).eraseToAnyPublisher() ) .map { $1 ?? $0 } + .removeDuplicates() .sink { [weak self] dialog in self?.show(dialog) } @@ -916,6 +917,15 @@ extension BrowserTabViewController: TabDelegate { // MARK: - Dialogs fileprivate func show(_ dialog: Tab.UserDialog?) { + guard activeUserDialogCancellable == nil || dialog == nil else { + // first hide a displayed dialog before showing another one + activeUserDialogCancellable = nil + DispatchQueue.main.async { [weak self] in + self?.show(dialog) + } + return + } + switch dialog?.dialog { case .basicAuthenticationChallenge(let query): activeUserDialogCancellable = showBasicAuthenticationChallenge(with: query) From 5cb9849992cde77e395d1a9d729a5451a40bd8ef Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 5 Apr 2024 15:28:23 +0200 Subject: [PATCH 017/221] Fixes the VPN restarting logic on update (#2545) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206999858530015/f ## Description: I noticed that we're getting increased number of failures when a new version of our app is released. After researching this I found that the mechanism we use for restarting the VPN can sometimes fail, but even if it doesn't fail it always fires failure pixels. This is likely caused by the fact that not disabling on-demand makes the connection try to start while we're stopping it and starting it ourselves. We'll now go back to disabling on-demand, but we'll now enable it optimistically right after starting the VPN again. --- .../NetworkProtectionTunnelController.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 27833760c2..95764249c9 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -599,15 +599,12 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr return } - await stop(tunnelManager: manager, disableOnDemand: true) + await stop(tunnelManager: manager) } @MainActor - private func stop(tunnelManager: NETunnelProviderManager, disableOnDemand: Bool) async { - if disableOnDemand { - // disable reconnect on demand if requested to stop - try? await self.disableOnDemand(tunnelManager: tunnelManager) - } + private func stop(tunnelManager: NETunnelProviderManager) async { + try? await self.disableOnDemand(tunnelManager: tunnelManager) switch tunnelManager.connection.status { case .connected, .connecting, .reasserting: @@ -625,8 +622,11 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr return } - await stop(tunnelManager: manager, disableOnDemand: false) + await stop(tunnelManager: manager) await start() + + // When restarting the tunnel we enable on-demand optimistically + try? await enableOnDemand(tunnelManager: manager) } // MARK: - On Demand & Kill Switch From 553d3ee52500b5c2b0fe4a87000734da205cdca2 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 8 Apr 2024 09:57:11 +0200 Subject: [PATCH 018/221] Improves underlying error pixel information (#2543) Task/Issue URL: https://app.asana.com/0/0/1206999233882390/f ## Description Some errors have more than 1 level of underlying error information. I've added code to handle this specifically in a way that we support including multiple levels of underlying errors, to get more detailed information. Additionally I'm doing some cleanup work to remove unnecessary code and start reducing debt. --- .../TransparentProxyControllerPixel.swift | 10 ++- ...TransparentProxyControllerPixelTests.swift | 13 +++- .../PixelKit/PixelKit+Parameters.swift | 28 ++++++- .../PixelKit/Sources/PixelKit/PixelKit.swift | 20 +---- .../Sources/PixelKit/PixelKitEventV2.swift | 37 ---------- .../PixelFireExpectations.swift | 20 +++-- .../PixelKitParametersTests.swift | 74 +++++++++++++++++++ .../NetworkProtectionPixelEventTests.swift | 24 +++--- 8 files changed, 141 insertions(+), 85 deletions(-) create mode 100644 LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift index b4c9b8cebb..97c759b30d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/Pixels/TransparentProxyControllerPixel.swift @@ -19,15 +19,17 @@ import Foundation import PixelKit -extension TransparentProxyController.StartError: PixelKitEventErrorDetails { - public var underlyingError: Error? { +extension TransparentProxyController.StartError: CustomNSError { + public var errorUserInfo: [String: Any] { switch self { case .failedToLoadConfiguration(let underlyingError), .failedToSaveConfiguration(let underlyingError), .failedToStartProvider(let underlyingError): - return underlyingError + return [ + NSUnderlyingErrorKey: underlyingError as NSError + ] default: - return nil + return [:] } } } diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift index bd21fe50d1..5a87e778c4 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionProxyTests/TransparentProxyControllerPixelTests.swift @@ -55,13 +55,20 @@ final class TransparentProxyControllerPixelTests: XCTestCase { static let startInitiatedFullPixelName = "m_mac_vpn_proxy_controller_start_initiated" static let startSuccessFullPixelName = "m_mac_vpn_proxy_controller_start_success" - enum TestError: PixelKitEventErrorDetails { + enum TestError: CustomNSError { case testError static let underlyingError = NSError(domain: "test", code: 1) - var underlyingError: Error? { - Self.underlyingError + public var errorUserInfo: [String: Any] { + switch self { + case .testError(let underlyingError): + return [ + NSUnderlyingErrorKey: underlyingError as NSError + ] + default: + return [:] + } } } diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index e368d38db5..777329156a 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -94,9 +94,9 @@ public extension Error { params[PixelKit.Parameters.errorCode] = "\(nsError.code)" params[PixelKit.Parameters.errorDomain] = nsError.domain - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { - params[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - params[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain + let underlyingErrorParameters = self.underlyingErrorParameters(for: nsError) + params.merge(underlyingErrorParameters) { first, _ in + return first } if let sqlErrorCode = nsError.userInfo["SQLiteResultCode"] as? NSNumber { @@ -110,4 +110,26 @@ public extension Error { return params } + /// Recursive call to add underlying error information + /// + func underlyingErrorParameters(for nsError: NSError, level: Int = 0) -> [String: String] { + if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { + let errorCodeParameterName = PixelKit.Parameters.underlyingErrorCode + (level == 0 ? "" : String(level + 1)) + let errorDomainParameterName = PixelKit.Parameters.underlyingErrorDomain + (level == 0 ? "" : String(level + 1)) + + let currentUnderlyingErrorParameters = [ + errorCodeParameterName: "\(underlyingError.code)", + errorDomainParameterName: underlyingError.domain + ] + + // Check if the underlying error has an underlying error of its own + let additionalParameters = underlyingErrorParameters(for: underlyingError, level: level + 1) + + return currentUnderlyingErrorParameters.merging(additionalParameters) { first, _ in + return first // Doesn't really matter as there should be no conflict of parameters + } + } + + return [:] + } } diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index ea94093fcb..eec1e1332c 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -375,24 +375,8 @@ public final class PixelKit { extension Dictionary where Key == String, Value == String { mutating func appendErrorPixelParams(error: Error) { - let nsError = error as NSError - - self[PixelKit.Parameters.errorCode] = "\(nsError.code)" - self[PixelKit.Parameters.errorDomain] = nsError.domain - - if let error = error as? PixelKitEventErrorDetails, - let underlyingError = error.underlyingError { - - let underlyingNSError = underlyingError as NSError - self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingNSError.code)" - self[PixelKit.Parameters.underlyingErrorDomain] = underlyingNSError.domain - } else if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError { - self[PixelKit.Parameters.underlyingErrorCode] = "\(underlyingError.code)" - self[PixelKit.Parameters.underlyingErrorDomain] = underlyingError.domain - } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { - self[PixelKit.Parameters.underlyingErrorCode] = "\(sqlErrorCode.intValue)" - self[PixelKit.Parameters.underlyingErrorDomain] = "NSSQLiteErrorDomain" + self.merge(error.pixelParameters) { _, second in + return second } } - } diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift index 811bf4643d..19525d0d0b 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift @@ -18,23 +18,6 @@ import Foundation -public protocol PixelKitEventErrorDetails: Error { - var underlyingError: Error? { get } -} - -extension PixelKitEventErrorDetails { - var underlyingErrorParameters: [String: String] { - guard let nsError = underlyingError as? NSError else { - return [:] - } - - return [ - PixelKit.Parameters.underlyingErrorCode: "\(nsError.code)", - PixelKit.Parameters.underlyingErrorDomain: nsError.domain - ] - } -} - /// New version of this protocol that allows us to maintain backwards-compatibility with PixelKitEvent /// /// This new implementation seeks to unify the handling of standard pixel parameters inside PixelKit. @@ -47,23 +30,3 @@ extension PixelKitEventErrorDetails { public protocol PixelKitEventV2: PixelKitEvent { var error: Error? { get } } - -extension PixelKitEventV2 { - var pixelParameters: [String: String] { - guard let error else { - return [:] - } - - let nsError = error as NSError - var parameters = [ - PixelKit.Parameters.errorCode: "\(nsError.code)", - PixelKit.Parameters.errorDomain: nsError.domain, - ] - - if let error = error as? PixelKitEventErrorDetails { - parameters.merge(error.underlyingErrorParameters, uniquingKeysWith: { $1 }) - } - - return parameters - } -} diff --git a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift index db56d6664b..1a1d8c2f64 100644 --- a/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift +++ b/LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelFireExpectations.swift @@ -27,20 +27,20 @@ import PixelKit public struct PixelFireExpectations { let pixelName: String var error: Error? - var underlyingError: Error? + var underlyingErrors: [Error] var customFields: [String: String]? /// Convenience initializer for cleaner semantics /// - public static func expect(pixelName: String, error: Error? = nil, underlyingError: Error? = nil, customFields: [String: String]? = nil) -> PixelFireExpectations { + public static func expect(pixelName: String, error: Error? = nil, underlyingErrors: [Error] = [], customFields: [String: String]? = nil) -> PixelFireExpectations { - .init(pixelName: pixelName, error: error, underlyingError: underlyingError, customFields: customFields) + .init(pixelName: pixelName, error: error, underlyingErrors: underlyingErrors, customFields: customFields) } - public init(pixelName: String, error: Error? = nil, underlyingError: Error? = nil, customFields: [String: String]? = nil) { + public init(pixelName: String, error: Error? = nil, underlyingErrors: [Error] = [], customFields: [String: String]? = nil) { self.pixelName = pixelName self.error = error - self.underlyingError = underlyingError + self.underlyingErrors = underlyingErrors self.customFields = customFields } @@ -52,9 +52,13 @@ public struct PixelFireExpectations { parameters[PixelKit.Parameters.errorDomain] = nsError.domain } - if let nsUnderlyingError = underlyingError as? NSError { - parameters[PixelKit.Parameters.underlyingErrorCode] = String(nsUnderlyingError.code) - parameters[PixelKit.Parameters.underlyingErrorDomain] = nsUnderlyingError.domain + for (index, error) in underlyingErrors.enumerated() { + let errorCodeParameterName = PixelKit.Parameters.underlyingErrorCode + (index == 0 ? "" : String(index + 1)) + let errorDomainParameterName = PixelKit.Parameters.underlyingErrorDomain + (index == 0 ? "" : String(index + 1)) + let nsError = error as NSError + + parameters[errorCodeParameterName] = String(nsError.code) + parameters[errorDomainParameterName] = nsError.domain } return parameters diff --git a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift new file mode 100644 index 0000000000..50d5e01d51 --- /dev/null +++ b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitParametersTests.swift @@ -0,0 +1,74 @@ +// +// PixelKitParametersTests.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 XCTest +@testable import PixelKit +import PixelKitTestingUtilities + +final class PixelKitParametersTests: XCTestCase { + + /// Test events for convenience + /// + private enum TestEvent: PixelKitEventV2 { + case errorEvent(error: Error) + + var name: String { + switch self { + case .errorEvent: + return "error_event" + } + } + + var parameters: [String: String]? { + nil + } + + var error: Error? { + switch self { + case .errorEvent(let error): + error + } + } + } + + /// Test that when firing pixels that include multiple levels of underlying error information, all levels + /// are properly included in the pixel. + /// + func testUnderlyingErrorInformationParameters() { + let underlyingError3 = NSError(domain: "test", code: 3) + let underlyingError2 = NSError( + domain: "test", + code: 2, + userInfo: [ + NSUnderlyingErrorKey: underlyingError3 as NSError + ]) + let topLevelError = NSError( + domain: "test", + code: 1, + userInfo: [ + NSUnderlyingErrorKey: underlyingError2 as NSError + ]) + + fire(TestEvent.errorEvent(error: topLevelError), + and: .expect(pixelName: "m_mac_error_event", + error: topLevelError, + underlyingErrors: [underlyingError2, underlyingError3]), + file: #filePath, + line: #line) + } +} diff --git a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift index 225fc36fcd..38483e97db 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift +++ b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift @@ -78,7 +78,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionControllerStartFailure(TestError.testError), and: .expect(pixelName: "m_mac_netp_controller_start_failure", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionControllerStartSuccess, @@ -92,7 +92,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartFailure(TestError.testError), and: .expect(pixelName: "m_mac_netp_tunnel_start_failure", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionTunnelStartSuccess, @@ -106,7 +106,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateFailure(TestError.testError), and: .expect(pixelName: "m_mac_netp_tunnel_update_failure", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionTunnelUpdateSuccess, @@ -164,7 +164,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchServerList(TestError.testError), and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_server_list", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseServerListResponse, @@ -178,7 +178,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchRegisteredServers(TestError.testError), and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_registered_servers", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseRegisteredServersResponse, @@ -196,25 +196,25 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToRedeemInviteCode(TestError.testError), and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_redeem_invite_code", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseRedeemResponse(TestError.testError), and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_redeem_response_failed", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToFetchLocations(TestError.testError), and: .expect(pixelName: "m_mac_netp_backend_api_error_failed_to_fetch_location_list", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionClientFailedToParseLocationsResponse(TestError.testError), and: .expect(pixelName: "m_mac_netp_backend_api_error_parsing_location_list_response_failed", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionClientInvalidAuthToken, @@ -277,7 +277,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotSetNetworkSettings(TestError.testError), and: .expect(pixelName: "m_mac_netp_wireguard_error_cannot_set_network_settings", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionWireguardErrorCannotStartWireguardBackend(code: 1), @@ -302,7 +302,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionRekeyFailure(TestError.testError), and: .expect(pixelName: "m_mac_netp_rekey_failure", error: TestError.testError, - underlyingError: TestError.underlyingError), + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, @@ -312,7 +312,7 @@ final class NetworkProtectionPixelEventTests: XCTestCase { fire(NetworkProtectionPixelEvent.networkProtectionUnhandledError(function: "function", line: 1, error: TestError.testError), and: .expect(pixelName: "m_mac_netp_unhandled_error", error: TestError.testError, - underlyingError: TestError.underlyingError, + underlyingErrors: [TestError.underlyingError], customFields: [ PixelKit.Parameters.function: "function", PixelKit.Parameters.line: "1", From e014c2e8c822c003a908b1694895b0df5282b8b8 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 8 Apr 2024 10:00:40 +0200 Subject: [PATCH 019/221] VPN: Cleanup authorize call (#2553) Task/Issue URL: https://app.asana.com/0/0/1206999858530020/f iOS PR: https://github.com/duckduckgo/iOS/pull/2685 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/763 ## Description Removes unused code. --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 3 +-- .../xcshareddata/xcschemes/sandbox-test-tool.xcscheme | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3269bcf0a5..47986ee67e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14425,8 +14425,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 132.0.1; + kind = revision; + revision = 65e4431f166ae802d61a6811c1496904b7228482; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cb0773b436..a7f1f56b32 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "73f68ee1c0dda3cd4a0b0cc3cc38a6cc7e605829", - "version" : "132.0.1" + "revision" : "65e4431f166ae802d61a6811c1496904b7228482" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index 41730d7069..eb7e5e26bb 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme @@ -1,7 +1,7 @@ + version = "1.8"> Date: Mon, 8 Apr 2024 10:08:22 +0200 Subject: [PATCH 020/221] Revert "VPN: Cleanup authorize call (#2553)" This reverts commit e014c2e8c822c003a908b1694895b0df5282b8b8. --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 3 ++- .../xcshareddata/xcschemes/sandbox-test-tool.xcscheme | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 47986ee67e..3269bcf0a5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14425,8 +14425,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = revision; - revision = 65e4431f166ae802d61a6811c1496904b7228482; + kind = exactVersion; + version = 132.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a7f1f56b32..cb0773b436 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,7 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "65e4431f166ae802d61a6811c1496904b7228482" + "revision" : "73f68ee1c0dda3cd4a0b0cc3cc38a6cc7e605829", + "version" : "132.0.1" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme index eb7e5e26bb..41730d7069 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/sandbox-test-tool.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> Date: Mon, 8 Apr 2024 10:52:27 +0200 Subject: [PATCH 021/221] VPN: Cleanup authorize call (#2565) Task/Issue URL: https://app.asana.com/0/0/1206999858530020/f iOS PR: https://github.com/duckduckgo/iOS/pull/2685 BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/763 ## NOTE: I'm re-creating https://github.com/duckduckgo/macos-browser/pull/2553 since it was merged before we had a new BSK version. ## Description Removes unused code. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../DuckDuckGo Privacy Browser App Store.xcscheme | 10 ++++++++++ .../xcschemes/DuckDuckGo Privacy Browser.xcscheme | 10 ++++++++++ .../xcshareddata/xcschemes/sandbox-test-tool.xcscheme | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 8 files changed, 27 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3269bcf0a5..1eb1ada673 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14426,7 +14426,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 132.0.1; + version = 132.0.2; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cb0773b436..ad6675f775 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "73f68ee1c0dda3cd4a0b0cc3cc38a6cc7e605829", - "version" : "132.0.1" + "revision" : "5199a6964e183c3d001b188286bbabeca93c8849", + "version" : "132.0.2" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme index 14f7550826..86fd422c11 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme @@ -177,6 +177,16 @@ ReferencedContainer = "container:LocalPackages/NetworkProtectionMac"> + + + + + + + + + version = "1.8"> Date: Mon, 8 Apr 2024 10:55:45 +0200 Subject: [PATCH 022/221] Removed the VPN waitlist beta pixels (#2555) Task/Issue URL: https://app.asana.com/0/0/1207008939337649/f iOS PR: ## Description Removes all VPN waitlist beta pixels. --- DuckDuckGo/Application/AppDelegate.swift | 1 - DuckDuckGo/Fire/View/FireViewController.swift | 2 +- .../MainWindow/MainViewController.swift | 9 ------- .../NavigationBar/View/MoreOptionsMenu.swift | 2 -- .../View/NavigationBarViewController.swift | 2 -- .../NetworkProtectionNavBarButtonModel.swift | 3 --- DuckDuckGo/Statistics/PixelEvent.swift | 24 ------------------- DuckDuckGo/Statistics/PixelParameters.swift | 8 ------- DuckDuckGo/Waitlist/Waitlist.swift | 4 +--- ...tlistTermsAndConditionsActionHandler.swift | 4 +--- 10 files changed, 3 insertions(+), 56 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 1ee442cfe3..1203e73af3 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -586,7 +586,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { #if NETWORK_PROTECTION if response.notification.request.identifier == NetworkProtectionWaitlist.notificationIdentifier { if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - DailyPixel.fire(pixel: .networkProtectionWaitlistNotificationTapped, frequency: .dailyAndCount) NetworkProtectionWaitlistViewControllerPresenter.show() } } diff --git a/DuckDuckGo/Fire/View/FireViewController.swift b/DuckDuckGo/Fire/View/FireViewController.swift index 1b82d96eb8..fee0006b48 100644 --- a/DuckDuckGo/Fire/View/FireViewController.swift +++ b/DuckDuckGo/Fire/View/FireViewController.swift @@ -17,7 +17,7 @@ // import Cocoa -@preconcurrency import Lottie +import Lottie import Combine @MainActor diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 17c166e932..7c5b9a1485 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -199,7 +199,6 @@ final class MainViewController: NSViewController { presentWaitlistThankYouPromptIfNecessary() #if NETWORK_PROTECTION - sendActiveNetworkProtectionWaitlistUserPixel() refreshNetworkProtectionMessages() #endif @@ -450,14 +449,6 @@ final class MainViewController: NSViewController { NSApp.mainMenuTyped.stopMenuItem.isEnabled = selectedTabViewModel.isLoading } -#if NETWORK_PROTECTION - private func sendActiveNetworkProtectionWaitlistUserPixel() { - if DefaultNetworkProtectionVisibility().waitlistIsOngoing { - DailyPixel.fire(pixel: .networkProtectionWaitlistUserActive, frequency: .dailyOnly) - } - } -#endif - func presentWaitlistThankYouPromptIfNecessary() { guard let window = self.view.window else { assertionFailure("Couldn't get main view controller's window") diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 4598e190e4..d2a782f91d 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -364,8 +364,6 @@ final class MoreOptionsMenu: NSMenu { } } #endif - - DailyPixel.fire(pixel: .networkProtectionWaitlistEntryPointMenuItemDisplayed, frequency: .dailyAndCount, includeAppVersionParameter: true) } else { networkProtectionFeatureVisibility.disableForWaitlistUsers() } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 2148a66473..011aa09845 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -344,12 +344,10 @@ final class NavigationBarViewController: NSViewController { if NetworkProtectionWaitlist().shouldShowWaitlistViewController { NetworkProtectionWaitlistViewControllerPresenter.show() - DailyPixel.fire(pixel: .networkProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) } else if NetworkProtectionKeychainTokenStore().isFeatureActivated { popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) } else { NetworkProtectionWaitlistViewControllerPresenter.show() - DailyPixel.fire(pixel: .networkProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) } } #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index 5ee8f863a0..cd6d4900a5 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -180,9 +180,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { let networkProtectionVisibility = DefaultNetworkProtectionVisibility() if networkProtectionVisibility.isNetworkProtectionBetaVisible() { if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - DailyPixel.fire(pixel: .networkProtectionWaitlistEntryPointToolbarButtonDisplayed, - frequency: .dailyOnly, - includeAppVersionParameter: true) showButton = true return } diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 7bd1a66adc..9f789182da 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -165,14 +165,6 @@ extension Pixel { case vpnBreakageReport(category: String, description: String, metadata: String) // VPN - case networkProtectionWaitlistUserActive - case networkProtectionWaitlistEntryPointMenuItemDisplayed - case networkProtectionWaitlistEntryPointToolbarButtonDisplayed - case networkProtectionWaitlistIntroDisplayed - case networkProtectionWaitlistNotificationShown - case networkProtectionWaitlistNotificationTapped - case networkProtectionWaitlistTermsAndConditionsDisplayed - case networkProtectionWaitlistTermsAndConditionsAccepted case networkProtectionRemoteMessageDisplayed(messageID: String) case networkProtectionRemoteMessageDismissed(messageID: String) case networkProtectionRemoteMessageOpened(messageID: String) @@ -573,22 +565,6 @@ extension Pixel.Event { case .vpnBreakageReport: return "m_mac_vpn_breakage_report" - case .networkProtectionWaitlistUserActive: - return "m_mac_netp_waitlist_user_active" - case .networkProtectionWaitlistEntryPointMenuItemDisplayed: - return "m_mac_netp_imp_settings_entry_menu_item" - case .networkProtectionWaitlistEntryPointToolbarButtonDisplayed: - return "m_mac_netp_imp_settings_entry_toolbar_button" - case .networkProtectionWaitlistIntroDisplayed: - return "m_mac_netp_imp_intro_screen" - case .networkProtectionWaitlistNotificationShown: - return "m_mac_netp_ev_waitlist_notification_shown" - case .networkProtectionWaitlistNotificationTapped: - return "m_mac_netp_ev_waitlist_notification_launched" - case .networkProtectionWaitlistTermsAndConditionsDisplayed: - return "m_mac_netp_imp_terms" - case .networkProtectionWaitlistTermsAndConditionsAccepted: - return "m_mac_netp_ev_terms_accepted" case .networkProtectionRemoteMessageDisplayed(let messageID): return "m_mac_netp_remote_message_displayed_\(messageID)" case .networkProtectionRemoteMessageDismissed(let messageID): diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 92f0ae69e8..3b7d89b5c4 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -124,14 +124,6 @@ extension Pixel.Event { .duckPlayerSettingAlways, .duckPlayerSettingNever, .duckPlayerSettingBackToDefault, - .networkProtectionWaitlistEntryPointMenuItemDisplayed, - .networkProtectionWaitlistEntryPointToolbarButtonDisplayed, - .networkProtectionWaitlistNotificationShown, - .networkProtectionWaitlistNotificationTapped, - .networkProtectionWaitlistTermsAndConditionsDisplayed, - .networkProtectionWaitlistTermsAndConditionsAccepted, - .networkProtectionWaitlistUserActive, - .networkProtectionWaitlistIntroDisplayed, .networkProtectionRemoteMessageDisplayed, .networkProtectionRemoteMessageDismissed, .networkProtectionRemoteMessageOpened, diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 8b91f0b8de..30b211e0ec 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -222,9 +222,7 @@ struct NetworkProtectionWaitlist: Waitlist { do { try await networkProtectionCodeRedemption.redeem(inviteCode) NotificationCenter.default.post(name: .networkProtectionWaitlistAccessChanged, object: nil) - sendInviteCodeAvailableNotification { - DailyPixel.fire(pixel: .networkProtectionWaitlistNotificationShown, frequency: .dailyAndCount) - } + sendInviteCodeAvailableNotification(completion: nil) completion(nil) } catch { assertionFailure("Failed to redeem invite code") diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index 0c0f8bb0cb..449ada2a92 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -36,15 +36,13 @@ struct NetworkProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAn var acceptedTermsAndConditions: Bool func didShow() { - DailyPixel.fire(pixel: .networkProtectionWaitlistTermsAndConditionsDisplayed, frequency: .dailyAndCount) + // Intentional no-op } mutating func didAccept() { acceptedTermsAndConditions = true // Remove delivered NetP notifications in case the user didn't click them. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [NetworkProtectionWaitlist.notificationIdentifier]) - - DailyPixel.fire(pixel: .networkProtectionWaitlistTermsAndConditionsAccepted, frequency: .dailyAndCount) } } From debc803101b32e44ed9ba91dd5ef7ac8634cc7f9 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Mon, 8 Apr 2024 10:08:57 +0100 Subject: [PATCH 023/221] QWD: Enable Hide/Show for Autofill Credit Card Number and CVV (#2539) Task/Issue URL:https://app.asana.com/0/276630244458377/1201712468973850/f **Description**: * Hides card CVV in Credit Card Autofill by default * Value can be shown/hidden using the provided button --- DuckDuckGo.xcodeproj/project.pbxproj | 8 ++ DuckDuckGo/Common/Localizables/UserText.swift | 3 + .../Common/View/SwiftUI/SecureTextField.swift | 90 +++++++++++++ DuckDuckGo/Localizable.xcstrings | 121 ++++++++++++++++++ ...PasswordManagementCreditCardItemView.swift | 70 +++++++++- .../PasswordManagementLoginItemView.swift | 39 +----- .../PasswordManagementViewController.swift | 8 +- 7 files changed, 300 insertions(+), 39 deletions(-) create mode 100644 DuckDuckGo/Common/View/SwiftUI/SecureTextField.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1eb1ada673..8391b71c55 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3228,6 +3228,9 @@ BBDFDC5A2B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; BBDFDC5C2B2B8D7000F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; BBDFDC5D2B2B8E2100F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */; }; + C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; + C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; + C1372EF62BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; C13909F02B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; C13909F12B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; @@ -4731,6 +4734,7 @@ B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; BBDFDC592B2B8A0900F62D90 /* DataBrokerProtectionExternalWaitlistPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionExternalWaitlistPixels.swift; sourceTree = ""; }; + C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; C13909FA2B861039001626ED /* AutofillActionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionPresenter.swift; sourceTree = ""; }; @@ -6728,6 +6732,7 @@ 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */, B6ABC5952B4861D4008343B9 /* FocusableTextField.swift */, B6F9BDE32B45CD1900677B33 /* ModalView.swift */, + C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */, ); path = SwiftUI; sourceTree = ""; @@ -10940,6 +10945,7 @@ 3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */, 7BFE955A2A9DF4550081ABE9 /* NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, + C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultErrorReporter.swift in Sources */, @@ -11834,6 +11840,7 @@ 4B957A762AC7AE700062CA31 /* FileDownloadManager.swift in Sources */, 4B957A772AC7AE700062CA31 /* BookmarkImport.swift in Sources */, 4BF0E5172AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, + C1372EF62BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 4B957A782AC7AE700062CA31 /* KeySetDictionary.swift in Sources */, B68D21CB2ACBC9A3002DA3C2 /* ContentBlockingMock.swift in Sources */, 4B957A792AC7AE700062CA31 /* HistoryTabExtension.swift in Sources */, @@ -12936,6 +12943,7 @@ EEC4A6692B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, AAC5E4D225D6A709007F5990 /* BookmarkList.swift in Sources */, B602E81D2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */, + C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */, 4B9292D12667123700AD2C21 /* BookmarkTableRowView.swift in Sources */, 85589E9427BFE1E70038AD11 /* FavoritesView.swift in Sources */, 85AC7ADB27BD628400FFB69B /* HomePage.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 49a5fb4d84..4033037606 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -921,6 +921,9 @@ struct UserText { static let copyPasswordTooltip = NSLocalizedString("autofill.copy-password", value: "Copy password", comment: "Tooltip for the Autofill panel's Copy Password button") static let showPasswordTooltip = NSLocalizedString("autofill.show-password", value: "Show password", comment: "Tooltip for the Autofill panel's Show Password button") static let hidePasswordTooltip = NSLocalizedString("autofill.hide-password", value: "Hide password", comment: "Tooltip for the Autofill panel's Hide Password button") + + static let autofillShowCardCvvTooltip = NSLocalizedString("autofill.show-card-cvv", value: "Show CVV", comment: "Tooltip for the Autofill panel's Show CVV button") + static let autofillHideCardCvvTooltip = NSLocalizedString("autofill.hide-card-cvv", value: "Hide CVV", comment: "Tooltip for the Autofill panel's Hide CVV button") static let databaseFactoryFailedMessage = NSLocalizedString("database.factory.failed.message", value: "There was an error initializing the database", comment: "Alert title when we fail to init database") static let databaseFactoryFailedInformative = NSLocalizedString("database.factory.failed.information", value: "Restart your Mac and try again", comment: "Info to restart macOS after database init failure") diff --git a/DuckDuckGo/Common/View/SwiftUI/SecureTextField.swift b/DuckDuckGo/Common/View/SwiftUI/SecureTextField.swift new file mode 100644 index 0000000000..c1359c8ce6 --- /dev/null +++ b/DuckDuckGo/Common/View/SwiftUI/SecureTextField.swift @@ -0,0 +1,90 @@ +// +// SecureTextField.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 SwiftUI + +/// View which uses the provided `isVisible` property to display either a `TextField` or a `SecureField` +struct SecureTextField: View { + + @Binding var textValue: String + let isVisible: Bool + var bottomPadding: CGFloat = 0 + + var body: some View { + if isVisible { + + TextField("", text: $textValue) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.bottom, bottomPadding) + } else { + + SecureField("", text: $textValue) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.bottom, bottomPadding) + } + } +} + +/// View which provides a Button styled to show/hide text with an action that toggles the provided `isVisible` property +struct SecureTextFieldButton: View { + + @Binding var isVisible: Bool + let toolTipHideText: String + let toolTipShowText: String + + var body: some View { + Button { + isVisible = !isVisible + } label: { + Image(.secureEyeToggle) + } + .buttonStyle(PlainButtonStyle()) + .tooltip(isVisible ? toolTipHideText : toolTipShowText) + } +} + +/// View which uses the provided `isVisible` property to display either the provided `text` or a string of `•` +struct HiddenText: View { + + let isVisible: Bool + let text: String + let hiddenTextLength: Int + + var body: some View { + if isVisible { + Text(text) + } else { + Text(text.isEmpty ? "" : String(repeating: "•", count: hiddenTextLength)) + } + } +} + +/// View which provides a Button styled to copy text which executes the provided `copyAction` +struct CopyButton: View { + + let copyAction: () -> Void + + var body: some View { + Button { + copyAction() + } label: { + Image(.copy) + } + .buttonStyle(PlainButtonStyle()) + } +} diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 288c5a5bc5..87e0f9dfb4 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -317,6 +317,7 @@ } }, "••••••••••••" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -3678,6 +3679,66 @@ } } }, + "autofill.hide-card-cvv" : { + "comment" : "Tooltip for the Autofill panel's Hide CVV button", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "CVV ausblenden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide CVV" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar CVV" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masquer CVV" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nascondi CVV" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "CVV verbergen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukryj CVV" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocultar CVV" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скрыть CVV-код" + } + } + } + }, "autofill.hide-password" : { "comment" : "Tooltip for the Autofill panel's Hide Password button", "extractionState" : "extracted_with_value", @@ -5298,6 +5359,66 @@ } } }, + "autofill.show-card-cvv" : { + "comment" : "Tooltip for the Autofill panel's Show CVV button", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "CVV anzeigen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show CVV" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar CVV" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afficher CVV" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostra CVV" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "CVV weergeven" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokaż CVV" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mostrar CVV" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Показать CVV-код" + } + } + } + }, "autofill.show-password" : { "comment" : "Tooltip for the Autofill panel's Show Password button", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift index c758fb88ce..367314c279 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift @@ -52,7 +52,11 @@ struct PasswordManagementCreditCardItemView: View { EditableCreditCardField(textFieldValue: $model.cardNumber, title: UserText.pmCardNumber) EditableCreditCardField(textFieldValue: $model.cardholderName, title: UserText.pmCardholderName) - EditableCreditCardField(textFieldValue: $model.cardSecurityCode, title: UserText.pmCardVerificationValue) + SecureEditableCreditCardField(textFieldValue: $model.cardSecurityCode, + title: UserText.pmCardVerificationValue, + hiddenTextLength: 3, + toolTipHideText: UserText.autofillHideCardCvvTooltip, + toolTipShowText: UserText.autofillShowCardCvvTooltip) // Expiration: @@ -237,5 +241,69 @@ private struct EditableCreditCardField: View { } } +} + +private struct SecureEditableCreditCardField: View { + + @EnvironmentObject var model: PasswordManagementCreditCardModel + + @Binding var textFieldValue: String + + @State private var isHovering = false + @State private var isVisible = false + + let title: String + let hiddenTextLength: Int + let toolTipHideText: String + let toolTipShowText: String + + var body: some View { + + if model.isInEditMode || !textFieldValue.isEmpty { + + VStack(alignment: .leading, spacing: 0) { + + Text(title) + .bold() + .padding(.bottom, 5) + + if model.isEditing || model.isNew { + + HStack { + + TextField("", text: $textFieldValue) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.bottom, interItemSpacing) + + } + .padding(.bottom, interItemSpacing) + + } else { + + HStack(spacing: 6) { + + HiddenText(isVisible: isVisible, text: textFieldValue, hiddenTextLength: hiddenTextLength) + + if (isHovering || isVisible) && textFieldValue != "" { + SecureTextFieldButton(isVisible: $isVisible, toolTipHideText: toolTipHideText, toolTipShowText: toolTipShowText) + } + + if isHovering { + CopyButton { + model.copy(textFieldValue) + } + } + Spacer() + } + .padding(.bottom, interItemSpacing) + } + + } + .onHover { + isHovering = $0 + } + + } + } } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift index cf1bb663e2..b6970449d5 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift @@ -360,25 +360,9 @@ private struct PasswordView: View { HStack { - if isPasswordVisible { + SecureTextField(textValue: $model.password, isVisible: isPasswordVisible) - TextField("", text: $model.password) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - } else { - - SecureField("", text: $model.password) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - } - - Button { - isPasswordVisible = !isPasswordVisible - } label: { - Image(.secureEyeToggle) - } - .buttonStyle(PlainButtonStyle()) - .tooltip(isPasswordVisible ? UserText.hidePasswordTooltip : UserText.showPasswordTooltip) + SecureTextFieldButton(isVisible: $isPasswordVisible, toolTipHideText: UserText.hidePasswordTooltip, toolTipShowText: UserText.showPasswordTooltip) .padding(.trailing, 10) } @@ -388,29 +372,16 @@ private struct PasswordView: View { HStack(alignment: .center, spacing: 6) { - if isPasswordVisible { - Text(model.password) - } else { - Text(model.password.isEmpty ? "" : "••••••••••••") - } + HiddenText(isVisible: isPasswordVisible, text: model.password, hiddenTextLength: 12) if (isHovering || isPasswordVisible) && model.password != "" { - Button { - isPasswordVisible = !isPasswordVisible - } label: { - Image(.secureEyeToggle) - } - .buttonStyle(PlainButtonStyle()) - .tooltip(isPasswordVisible ? UserText.hidePasswordTooltip : UserText.showPasswordTooltip) + SecureTextFieldButton(isVisible: $isPasswordVisible, toolTipHideText: UserText.hidePasswordTooltip, toolTipShowText: UserText.showPasswordTooltip) } if isHovering && model.password != "" { - Button { + CopyButton { model.copy(model.password) - } label: { - Image(.copy) } - .buttonStyle(PlainButtonStyle()) .tooltip(UserText.copyPasswordTooltip) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index ab6ea02921..9c9600f5b8 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -818,10 +818,10 @@ final class PasswordManagementViewController: NSViewController { // swiftlint:enable function_body_length private func createNewSecureVaultItemMenu() -> NSMenu { - NSMenu { - NSMenuItem(title: UserText.pmNewLogin, action: #selector(createNewLogin)).withImage(.loginGlyph) - NSMenuItem(title: UserText.pmNewIdentity, action: #selector(createNewIdentity)).withImage(.identityGlyph) - NSMenuItem(title: UserText.pmNewCard, action: #selector(createNewCreditCard)).withImage(.creditCardGlyph) + return NSMenu { + NSMenuItem(title: UserText.pmNewLogin, action: #selector(createNewLogin), target: self).withImage(.loginGlyph) + NSMenuItem(title: UserText.pmNewIdentity, action: #selector(createNewIdentity), target: self).withImage(.identityGlyph) + NSMenuItem(title: UserText.pmNewCard, action: #selector(createNewCreditCard), target: self).withImage(.creditCardGlyph) } } From 70c84192d9c30a8dcff38b7e59affa36322fdabe Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 8 Apr 2024 17:07:20 +0200 Subject: [PATCH 024/221] Bye bye NETWORK_PROTECTION (#2509) Task/Issue URL: https://app.asana.com/0/0/1206935409526736/f ## Description: Removes the `NETWORK_PROTECTION` feature flag. --- .../App/DBP/DuckDuckGoDBPAgent.xcconfig | 2 +- .../DBP/DuckDuckGoDBPAgentAppStore.xcconfig | 2 +- Configuration/App/DuckDuckGo.xcconfig | 2 +- .../App/DuckDuckGoPrivacyPro.xcconfig | 2 +- .../NetworkProtection/DuckDuckGoVPN.xcconfig | 8 ++-- .../DuckDuckGoVPNAppStore.xcconfig | 8 ++-- Configuration/Common.xcconfig | 2 +- .../NetworkProtectionAppExtension.xcconfig | 8 ++-- .../NetworkProtectionSystemExtension.xcconfig | 8 ++-- .../VPNProxyExtension.xcconfig | 8 ++-- Configuration/Tests/IntegrationTests.xcconfig | 2 +- Configuration/Tests/UnitTests.xcconfig | 2 +- .../Tests/UnitTestsAppStore.xcconfig | 2 +- DuckDuckGo/Application/AppDelegate.swift | 18 +------ DuckDuckGo/Application/URLEventHandler.swift | 12 ----- .../Model/HomePageContinueSetUpModel.swift | 22 +-------- .../MainWindow/MainViewController.swift | 9 ---- DuckDuckGo/Menus/MainMenu.swift | 13 ----- DuckDuckGo/Menus/MainMenuActions.swift | 2 - DuckDuckGo/NavigationBar/PinningManager.swift | 14 ------ .../NavigationBar/View/MoreOptionsMenu.swift | 32 ------------- .../View/NavigationBarPopovers.swift | 22 --------- .../View/NavigationBarViewController.swift | 48 ------------------- .../View/NetPPopoverManagerMock.swift | 2 +- .../AppLauncher.swift | 4 -- .../NetworkProtectionPixelEvent.swift | 4 -- ...UserDefaults+NetworkProtectionShared.swift | 4 -- .../NetworkProtectionOptionKeyExtension.swift | 4 -- .../EventMapping+NetworkProtectionError.swift | 4 -- ...NetworkProtectionInviteCodeViewModel.swift | 4 -- .../NetworkProtectionInviteDialog.swift | 4 -- .../NetworkProtectionInvitePresenter.swift | 4 -- .../LoginItem+NetworkProtection.swift | 4 -- ...rkProtection+ConvenienceInitializers.swift | 4 -- .../NetworkProtectionAppEvents.swift | 3 -- ...etworkProtectionControllerErrorStore.swift | 4 -- .../NetworkProtectionDebugMenu.swift | 4 -- .../NetworkProtectionDebugUtilities.swift | 4 -- .../NetworkProtectionNavBarButtonModel.swift | 4 -- ...etworkProtectionNavBarPopoverManager.swift | 7 --- .../NetworkProtectionOnboardingMenu.swift | 4 -- ...NetworkProtectionSimulateFailureMenu.swift | 4 -- .../NetworkProtectionTunnelController.swift | 4 -- ...tionWaitlistFeatureFlagOverridesMenu.swift | 4 -- ...tworkProtectionVPNCountryLabelsModel.swift | 4 -- .../VPNLocationPreferenceItem.swift | 4 -- .../VPNLocationPreferenceItemModel.swift | 4 -- .../VPNLocation/VPNLocationView.swift | 4 -- .../VPNLocation/VPNLocationViewModel.swift | 4 -- .../VPNLocationsHostingViewController.swift | 4 -- ...NetworkProtectionIPCTunnelController.swift | 4 -- .../NetworkProtectionRemoteMessaging.swift | 4 -- ...rkProtectionSubscriptionEventHandler.swift | 2 +- DuckDuckGo/Preferences/Model/AboutModel.swift | 8 ---- .../Model/PreferencesSection.swift | 8 ---- .../Model/PreferencesSidebarModel.swift | 8 ---- .../Model/VPNPreferencesModel.swift | 4 -- .../View/PreferencesAboutView.swift | 2 - .../View/PreferencesRootView.swift | 7 --- .../Preferences/View/PreferencesVPNView.swift | 4 -- .../View/PreferencesViewController.swift | 3 -- DuckDuckGo/Tab/Model/Tab.swift | 15 +----- .../VPNFeedbackForm/VPNFeedbackCategory.swift | 4 -- .../VPNFeedbackForm/VPNFeedbackFormView.swift | 4 -- .../VPNFeedbackFormViewController.swift | 4 -- .../VPNFeedbackFormViewModel.swift | 4 -- .../VPNFeedbackForm/VPNFeedbackSender.swift | 4 -- .../VPNMetadataCollector.swift | 4 -- .../Waitlist/Models/WaitlistViewModel.swift | 4 -- .../NetworkProtectionFeatureDisabler.swift | 4 -- .../NetworkProtectionFeatureVisibility.swift | 4 -- .../Waitlist/Views/WaitlistDialogView.swift | 4 -- .../Views/WaitlistModalViewController.swift | 4 -- .../Waitlist/Views/WaitlistRootView.swift | 4 -- .../EnableWaitlistFeatureView.swift | 8 ---- .../WaitlistSteps/InvitedToWaitlistView.swift | 8 ---- .../WaitlistSteps/JoinWaitlistView.swift | 8 ---- .../WaitlistSteps/JoinedWaitlistView.swift | 8 ---- .../WaitlistTermsAndConditionsView.swift | 8 ---- .../WaitlistViewControllerPresenter.swift | 8 ---- DuckDuckGo/Waitlist/Waitlist.swift | 4 -- .../WaitlistFeatureSetupHandler.swift | 8 ---- ...tlistTermsAndConditionsActionHandler.swift | 8 ---- .../View/WindowControllersManager.swift | 2 - .../HomePage/ContinueSetUpModelTests.swift | 37 +++----------- UnitTests/Menus/MoreOptionsMenuTests.swift | 34 ------------- .../LocalPinningManagerTests.swift | 9 ---- .../View/NavigationBarPopoversTests.swift | 5 -- .../NetworkProtectionPixelEventTests.swift | 4 -- ...etworkProtectionRemoteMessagingTests.swift | 4 -- .../VPNFeedbackFormViewModelTests.swift | 4 -- .../MockNetworkProtectionCodeRedeemer.swift | 4 -- .../MockWaitlistFeatureSetupHandler.swift | 4 -- ...tlistTermsAndConditionsActionHandler.swift | 4 -- .../Waitlist/WaitlistViewModelTests.swift | 4 -- 95 files changed, 42 insertions(+), 614 deletions(-) diff --git a/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig b/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig index be90b0c9b3..edfa557220 100644 --- a/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig +++ b/Configuration/App/DBP/DuckDuckGoDBPAgent.xcconfig @@ -47,7 +47,7 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS DBP Agent - Review PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = macOS DBP Agent - Release -FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION +FEATURE_FLAGS = FEEDBACK DBP GCC_PREPROCESSOR_DEFINITIONS[arch=*][sdk=*] = DBP=1 NETP_SYSTEM_EXTENSION=1 GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DBP=1 NETP_SYSTEM_EXTENSION=1 DEBUG=1 CI=1 $(inherited) diff --git a/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig b/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig index 154bd9cf6b..e19a23209a 100644 --- a/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig +++ b/Configuration/App/DBP/DuckDuckGoDBPAgentAppStore.xcconfig @@ -49,7 +49,7 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent.review macos PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.DBP.backgroundAgent macos -FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION +FEATURE_FLAGS = FEEDBACK DBP GCC_PREPROCESSOR_DEFINITIONS[arch=*][sdk=*] = DBP=1 NETP_SYSTEM_EXTENSION=1 GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DBP=1 NETP_SYSTEM_EXTENSION=1 DEBUG=1 CI=1 $(inherited) diff --git a/Configuration/App/DuckDuckGo.xcconfig b/Configuration/App/DuckDuckGo.xcconfig index 710327265e..e0490eb81d 100644 --- a/Configuration/App/DuckDuckGo.xcconfig +++ b/Configuration/App/DuckDuckGo.xcconfig @@ -26,7 +26,7 @@ CODE_SIGN_IDENTITY[sdk=macosx*] = Developer ID Application CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE DBP SUBSCRIPTION STRIPE +FEATURE_FLAGS = FEEDBACK SPARKLE DBP SUBSCRIPTION STRIPE PRODUCT_NAME_PREFIX = DuckDuckGo diff --git a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig index eaa4fc8497..b4b2ca9ec4 100644 --- a/Configuration/App/DuckDuckGoPrivacyPro.xcconfig +++ b/Configuration/App/DuckDuckGoPrivacyPro.xcconfig @@ -21,6 +21,6 @@ #include "DuckDuckGo.xcconfig" -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION SPARKLE SUBSCRIPTION DBP SUBSCRIPTION_OVERRIDE_ENABLED STRIPE +FEATURE_FLAGS = FEEDBACK SPARKLE SUBSCRIPTION DBP SUBSCRIPTION_OVERRIDE_ENABLED STRIPE PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) Privacy Pro PRODUCT_MODULE_NAME = $(PRIVACY_PRO_PRODUCT_MODULE_NAME_OVERRIDE:default=$(DEFAULT_PRODUCT_MODULE_NAME)) diff --git a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig index b3b7f7ef35..1840da7701 100644 --- a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig +++ b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig @@ -49,10 +49,10 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS NetP VPN App - Review (XPC) PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = macOS NetP VPN App - Release (XPC) -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION SUBSCRIPTION SWIFT_OBJC_BRIDGING_HEADER = SKIP_INSTALL = YES diff --git a/Configuration/App/NetworkProtection/DuckDuckGoVPNAppStore.xcconfig b/Configuration/App/NetworkProtection/DuckDuckGoVPNAppStore.xcconfig index cad39a4d7e..82c6a46644 100644 --- a/Configuration/App/NetworkProtection/DuckDuckGoVPNAppStore.xcconfig +++ b/Configuration/App/NetworkProtection/DuckDuckGoVPNAppStore.xcconfig @@ -50,10 +50,10 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent macos PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = match AppStore com.duckduckgo.mobile.ios.vpn.agent.review macos -FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[arch=*][sdk=*] = SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = SUBSCRIPTION ENABLE_APP_SANDBOX = YES SWIFT_OBJC_BRIDGING_HEADER = diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig index 392d63d532..08829b84c0 100644 --- a/Configuration/Common.xcconfig +++ b/Configuration/Common.xcconfig @@ -21,7 +21,7 @@ COMBINE_HIDPI_IMAGES = YES DEVELOPMENT_TEAM = HKE973VLUW DEVELOPMENT_TEAM[config=CI][sdk=*] = -FEATURE_FLAGS = FEEDBACK DBP NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS = FEEDBACK DBP SUBSCRIPTION GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig index 2fb095fc56..8b58584eab 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionAppExtension.xcconfig @@ -30,10 +30,10 @@ GENERATE_INFOPLIST_FILE = YES INFOPLIST_FILE = NetworkProtectionAppExtension/Info.plist INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. -FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(TUNNEL_EXTENSION_BUNDLE_ID) diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig index 325f9024b7..25bbdee54d 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig @@ -31,10 +31,10 @@ INFOPLIST_FILE = NetworkProtectionSystemExtension/Info.plist INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. INFOPLIST_KEY_NSSystemExtensionUsageDescription = DuckDuckGo VPN -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION SUBSCRIPTION PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(SYSEX_BUNDLE_ID) PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) diff --git a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig index 5f70d87091..c2b25dbe47 100644 --- a/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/VPNProxyExtension.xcconfig @@ -30,10 +30,10 @@ GENERATE_INFOPLIST_FILE = YES INFOPLIST_FILE = VPNProxyExtension/Info.plist INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. -FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETWORK_EXTENSION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETWORK_EXTENSION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETWORK_EXTENSION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETWORK_EXTENSION PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(PROXY_EXTENSION_BUNDLE_ID) diff --git a/Configuration/Tests/IntegrationTests.xcconfig b/Configuration/Tests/IntegrationTests.xcconfig index cee1523e37..7bbf931ba5 100644 --- a/Configuration/Tests/IntegrationTests.xcconfig +++ b/Configuration/Tests/IntegrationTests.xcconfig @@ -17,7 +17,7 @@ MACOSX_DEPLOYMENT_TARGET = 11.4 -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP +FEATURE_FLAGS = FEEDBACK DBP INFOPLIST_FILE = IntegrationTests/Info.plist PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.Integration-Tests diff --git a/Configuration/Tests/UnitTests.xcconfig b/Configuration/Tests/UnitTests.xcconfig index b09dee6dc1..a6e5d79a1d 100644 --- a/Configuration/Tests/UnitTests.xcconfig +++ b/Configuration/Tests/UnitTests.xcconfig @@ -17,7 +17,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP SUBSCRIPTION +FEATURE_FLAGS = FEEDBACK DBP SUBSCRIPTION INFOPLIST_FILE = UnitTests/Info.plist PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.DuckDuckGoTests diff --git a/Configuration/Tests/UnitTestsAppStore.xcconfig b/Configuration/Tests/UnitTestsAppStore.xcconfig index b31177d987..0f966ba610 100644 --- a/Configuration/Tests/UnitTestsAppStore.xcconfig +++ b/Configuration/Tests/UnitTestsAppStore.xcconfig @@ -16,7 +16,7 @@ #include "UnitTests.xcconfig" #include "../AppStore.xcconfig" -FEATURE_FLAGS = FEEDBACK NETWORK_PROTECTION DBP SUBSCRIPTION +FEATURE_FLAGS = FEEDBACK DBP SUBSCRIPTION PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.mobile.ios.DuckDuckGoTests diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 1203e73af3..c0307baaa8 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -33,9 +33,7 @@ import SyncDataProviders import UserNotifications import Lottie -#if NETWORK_PROTECTION import NetworkProtection -#endif #if SUBSCRIPTION import Subscription @@ -82,7 +80,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? -#if NETWORK_PROTECTION && SUBSCRIPTION +#if SUBSCRIPTION // Needs to be lazy as indirectly depends on AppDelegate private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() #endif @@ -287,14 +285,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { UserDefaultsWrapper.clearRemovedKeys() -#if NETWORK_PROTECTION && SUBSCRIPTION +#if SUBSCRIPTION networkProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() #endif -#if NETWORK_PROTECTION NetworkProtectionAppEvents().applicationDidFinishLaunching() UNUserNotificationCenter.current().delegate = self -#endif #if DBP && SUBSCRIPTION dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() @@ -315,13 +311,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() -#if NETWORK_PROTECTION NetworkProtectionWaitlist().fetchNetworkProtectionInviteCodeIfAvailable { _ in // Do nothing when code fetching fails, as the app will try again later } NetworkProtectionAppEvents().applicationDidBecomeActive() -#endif #if DBP DataBrokerProtectionAppEvents().applicationDidBecomeActive() @@ -372,7 +366,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } private static func setUpPixelKit(dryRun: Bool) { -#if NETWORK_PROTECTION #if APPSTORE let source = "browser-appstore" #else @@ -395,7 +388,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { onComplete(error == nil, error) } } -#endif } // MARK: - Sync @@ -568,8 +560,6 @@ func updateSubscriptionStatus() { #endif } -#if NETWORK_PROTECTION || DBP - extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, @@ -583,13 +573,11 @@ extension AppDelegate: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == UNNotificationDefaultActionIdentifier { -#if NETWORK_PROTECTION if response.notification.request.identifier == NetworkProtectionWaitlist.notificationIdentifier { if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { NetworkProtectionWaitlistViewControllerPresenter.show() } } -#endif #if DBP if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { @@ -602,5 +590,3 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } } - -#endif diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 7ae97a807f..e261efdf12 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -20,9 +20,7 @@ import Common import Foundation import AppKit -#if NETWORK_PROTECTION import NetworkProtectionUI -#endif #if DBP import DataBrokerProtection @@ -104,11 +102,9 @@ final class URLEventHandler { } private static func openURL(_ url: URL) { -#if NETWORK_PROTECTION if url.scheme?.isNetworkProtectionScheme == true { handleNetworkProtectionURL(url) } -#endif #if DBP if url.scheme?.isDataBrokerProtectionScheme == true { @@ -131,18 +127,12 @@ final class URLEventHandler { return } -#if NETWORK_PROTECTION || DBP if url.scheme?.isNetworkProtectionScheme == false && url.scheme?.isDataBrokerProtectionScheme == false { WaitlistModalDismisser.dismissWaitlistModalViewControllerIfNecessary(url) WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) } -#else - WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) -#endif } -#if NETWORK_PROTECTION - /// Handles NetP URLs /// private static func handleNetworkProtectionURL(_ url: URL) { @@ -173,8 +163,6 @@ final class URLEventHandler { } } -#endif - #if DBP /// Handles DBP URLs /// diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index 41c8bc3075..eb693541b9 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -20,11 +20,8 @@ import AppKit import BrowserServicesKit import Common import Foundation - -#if NETWORK_PROTECTION import NetworkProtection import NetworkProtectionUI -#endif extension HomePage.Models { @@ -221,10 +218,8 @@ extension HomePage.Models { case .surveyDay14: shouldShowSurveyDay14 = false case .networkProtectionRemoteMessage(let message): -#if NETWORK_PROTECTION homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: message) Pixel.fire(.networkProtectionRemoteMessageDismissed(messageID: message.id)) -#endif case .dataBrokerProtectionRemoteMessage(let message): #if DBP homePageRemoteMessaging.dataBrokerProtectionRemoteMessaging.dismiss(message: message) @@ -257,7 +252,6 @@ extension HomePage.Models { } #endif -#if NETWORK_PROTECTION for message in homePageRemoteMessaging.networkProtectionRemoteMessaging.presentableRemoteMessages() { features.append(.networkProtectionRemoteMessage(message)) DailyPixel.fire( @@ -265,7 +259,6 @@ extension HomePage.Models { frequency: .dailyOnly ) } -#endif if waitlistBetaThankYouPresenter.canShowVPNCard { features.append(.vpnThankYou) @@ -445,7 +438,6 @@ extension HomePage.Models { } @MainActor private func handle(remoteMessage: NetworkProtectionRemoteMessage) { -#if NETWORK_PROTECTION guard let actionType = remoteMessage.action.actionType else { Pixel.fire(.networkProtectionRemoteMessageDismissed(messageID: remoteMessage.id)) homePageRemoteMessaging.networkProtectionRemoteMessaging.dismiss(message: remoteMessage) @@ -467,7 +459,6 @@ extension HomePage.Models { refreshFeaturesMatrix() } } -#endif } @MainActor private func handle(remoteMessage: DataBrokerProtectionRemoteMessage) { @@ -649,32 +640,23 @@ extension HomePage.Models { struct HomePageRemoteMessaging { static func defaultMessaging() -> HomePageRemoteMessaging { -#if NETWORK_PROTECTION && DBP +#if DBP return HomePageRemoteMessaging( networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: .netP, dataBrokerProtectionRemoteMessaging: DefaultDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: .dbp ) -#elseif NETWORK_PROTECTION +#else return HomePageRemoteMessaging( networkProtectionRemoteMessaging: DefaultNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: .netP ) -#elseif DBP - return HomePageRemoteMessaging( - dataBrokerProtectionRemoteMessaging: DefaultDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: .dbp - ) -#else - return HomePageRemoteMessaging() #endif } -#if NETWORK_PROTECTION let networkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging let networkProtectionUserDefaults: UserDefaults -#endif #if DBP let dataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 7c5b9a1485..d2442a68c2 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -20,11 +20,8 @@ import Cocoa import Carbon.HIToolbox import Combine import Common - -#if NETWORK_PROTECTION import NetworkProtection import NetworkProtectionIPC -#endif final class MainViewController: NSViewController { private lazy var mainView = MainView(frame: NSRect(x: 0, y: 0, width: 600, height: 660)) @@ -64,7 +61,6 @@ final class MainViewController: NSViewController { tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) -#if NETWORK_PROTECTION let networkProtectionPopoverManager: NetPPopoverManager = { #if DEBUG guard case .normal = NSApp.runType else { @@ -100,9 +96,6 @@ final class MainViewController: NSViewController { }() navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter) -#else - navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, autofillPopoverPresenter: AutofillPopoverPresenter) -#endif browserTabViewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) findInPageViewController = FindInPageViewController.create() @@ -227,13 +220,11 @@ final class MainViewController: NSViewController { } } -#if NETWORK_PROTECTION private let networkProtectionMessaging = DefaultNetworkProtectionRemoteMessaging() func refreshNetworkProtectionMessages() { networkProtectionMessaging.fetchRemoteMessages() } -#endif #if DBP private let dataBrokerProtectionMessaging = DefaultDataBrokerProtectionRemoteMessaging() diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 2e6c0d2639..31d25d3d31 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -24,10 +24,7 @@ import OSLog // swiftlint:disable:this enforce_os_log_wrapper import SwiftUI import WebKit import Configuration - -#if NETWORK_PROTECTION import NetworkProtection -#endif #if SUBSCRIPTION import Subscription @@ -85,9 +82,7 @@ import SubscriptionUI let toggleBookmarksShortcutMenuItem = NSMenuItem(title: UserText.mainMenuViewShowBookmarksShortcut, action: #selector(MainViewController.toggleBookmarksShortcut), keyEquivalent: "K") let toggleDownloadsShortcutMenuItem = NSMenuItem(title: UserText.mainMenuViewShowDownloadsShortcut, action: #selector(MainViewController.toggleDownloadsShortcut), keyEquivalent: "J") -#if NETWORK_PROTECTION let toggleNetworkProtectionShortcutMenuItem = NSMenuItem(title: UserText.showNetworkProtectionShortcut, action: #selector(MainViewController.toggleNetworkProtectionShortcut), keyEquivalent: "N") -#endif // MARK: Window let windowsMenu = NSMenu(title: UserText.mainMenuWindow) @@ -270,9 +265,7 @@ import SubscriptionUI toggleBookmarksShortcutMenuItem toggleDownloadsShortcutMenuItem -#if NETWORK_PROTECTION toggleNetworkProtectionShortcutMenuItem -#endif NSMenuItem.separator() @@ -398,10 +391,8 @@ import SubscriptionUI override func update() { super.update() -#if NETWORK_PROTECTION // To be safe, hide the NetP shortcut menu item by default. toggleNetworkProtectionShortcutMenuItem.isHidden = true -#endif updateHomeButtonMenuItem() updateBookmarksBarMenuItem() @@ -549,14 +540,12 @@ import SubscriptionUI toggleBookmarksShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .bookmarks) toggleDownloadsShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .downloads) -#if NETWORK_PROTECTION if DefaultNetworkProtectionVisibility().isVPNVisible() { toggleNetworkProtectionShortcutMenuItem.isHidden = false toggleNetworkProtectionShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .networkProtection) } else { toggleNetworkProtectionShortcutMenuItem.isHidden = true } -#endif } } @@ -616,12 +605,10 @@ import SubscriptionUI .submenu(DataBrokerProtectionDebugMenu()) #endif -#if NETWORK_PROTECTION if case .normal = NSApp.runType { NSMenuItem(title: "VPN") .submenu(NetworkProtectionDebugMenu()) } -#endif NSMenuItem(title: "Trigger Fatal Error", action: #selector(MainViewController.triggerFatalError)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 51141f5da2..171e6699c1 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -422,9 +422,7 @@ extension MainViewController { } @objc func toggleNetworkProtectionShortcut(_ sender: Any) { -#if NETWORK_PROTECTION LocalPinningManager.shared.togglePinning(for: .networkProtection) -#endif } // MARK: - History diff --git a/DuckDuckGo/NavigationBar/PinningManager.swift b/DuckDuckGo/NavigationBar/PinningManager.swift index d2cc6ef6bb..37eceb098c 100644 --- a/DuckDuckGo/NavigationBar/PinningManager.swift +++ b/DuckDuckGo/NavigationBar/PinningManager.swift @@ -17,20 +17,14 @@ // import Foundation - -#if NETWORK_PROTECTION import NetworkProtection -#endif enum PinnableView: String { case autofill case bookmarks case downloads case homeButton - -#if NETWORK_PROTECTION case networkProtection -#endif } protocol PinningManager { @@ -45,11 +39,7 @@ protocol PinningManager { final class LocalPinningManager: PinningManager { -#if NETWORK_PROTECTION static let shared = LocalPinningManager(networkProtectionFeatureActivation: NetworkProtectionKeychainTokenStore()) -#else - static let shared = LocalPinningManager() -#endif static let pinnedViewChangedNotificationViewTypeKey = "pinning.pinnedViewChanged.viewType" @@ -59,13 +49,11 @@ final class LocalPinningManager: PinningManager { @UserDefaultsWrapper(key: .manuallyToggledPinnedViews, defaultValue: []) private var manuallyToggledPinnedViewsStrings: [String] -#if NETWORK_PROTECTION private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation init(networkProtectionFeatureActivation: NetworkProtectionFeatureActivation) { self.networkProtectionFeatureActivation = networkProtectionFeatureActivation } -#endif func togglePinning(for view: PinnableView) { flagAsManuallyToggled(view) @@ -122,10 +110,8 @@ final class LocalPinningManager: PinningManager { case .bookmarks: return isPinned(.bookmarks) ? UserText.hideBookmarksShortcut : UserText.showBookmarksShortcut case .downloads: return isPinned(.downloads) ? UserText.hideDownloadsShortcut : UserText.showDownloadsShortcut case .homeButton: return "" -#if NETWORK_PROTECTION case .networkProtection: return isPinned(.networkProtection) ? UserText.hideNetworkProtectionShortcut : UserText.showNetworkProtectionShortcut -#endif } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index d2a782f91d..ed8a2e2b76 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -20,10 +20,7 @@ import Cocoa import Combine import Common import BrowserServicesKit - -#if NETWORK_PROTECTION import NetworkProtection -#endif #if SUBSCRIPTION import Subscription @@ -64,15 +61,12 @@ final class MoreOptionsMenu: NSMenu { private lazy var sharingMenu: NSMenu = SharingMenu(title: UserText.shareMenuItem) private lazy var accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) -#if NETWORK_PROTECTION private let networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility -#endif required init(coder: NSCoder) { fatalError("MoreOptionsMenu: Bad initializer") } -#if NETWORK_PROTECTION init(tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), passwordManagerCoordinator: PasswordManagerCoordinator, @@ -95,28 +89,6 @@ final class MoreOptionsMenu: NSMenu { setupMenuItems() } -#else - init(tabCollectionViewModel: TabCollectionViewModel, - emailManager: EmailManager = EmailManager(), - passwordManagerCoordinator: PasswordManagerCoordinator, - sharingMenu: NSMenu? = nil, - internalUserDecider: InternalUserDecider) { - - self.tabCollectionViewModel = tabCollectionViewModel - self.emailManager = emailManager - self.passwordManagerCoordinator = passwordManagerCoordinator - self.internalUserDecider = internalUserDecider - - super.init(title: "") - - if let sharingMenu { - self.sharingMenu = sharingMenu - } - self.emailManager.requestDelegate = self - - setupMenuItems() - } -#endif let zoomMenuItem = NSMenuItem(title: UserText.zoom, action: nil, keyEquivalent: "") @@ -341,7 +313,6 @@ final class MoreOptionsMenu: NSMenu { let subscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability() #endif -#if NETWORK_PROTECTION if networkProtectionFeatureVisibility.isNetworkProtectionBetaVisible() { let networkProtectionItem: NSMenuItem @@ -367,7 +338,6 @@ final class MoreOptionsMenu: NSMenu { } else { networkProtectionFeatureVisibility.disableForWaitlistUsers() } -#endif // NETWORK_PROTECTION #if DBP let dbpVisibility = DefaultDataBrokerProtectionFeatureVisibility() @@ -482,7 +452,6 @@ final class MoreOptionsMenu: NSMenu { } -#if NETWORK_PROTECTION private func makeNetworkProtectionItem() -> NSMenuItem { let networkProtectionItem = NSMenuItem(title: "", action: #selector(showNetworkProtectionStatus(_:)), keyEquivalent: "") .targetting(self) @@ -492,7 +461,6 @@ final class MoreOptionsMenu: NSMenu { return networkProtectionItem } -#endif } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift index b8607c8f85..8eee150a17 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift @@ -19,19 +19,15 @@ import Foundation import BrowserServicesKit import AppKit - -#if NETWORK_PROTECTION import Combine import NetworkProtection import NetworkProtectionUI import NetworkProtectionIPC -#endif protocol PopoverPresenter { func show(_ popover: NSPopover, positionedBelow view: NSView) } -#if NETWORK_PROTECTION protocol NetPPopoverManager: AnyObject { var ipcClient: NetworkProtectionIPCClient { get } var isShown: Bool { get } @@ -41,7 +37,6 @@ protocol NetPPopoverManager: AnyObject { func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) } -#endif extension PopoverPresenter { func show(_ popover: NSPopover, positionedBelow view: NSView) { @@ -63,7 +58,6 @@ final class NavigationBarPopovers: PopoverPresenter { private(set) var autofillPopoverPresenter: AutofillPopoverPresenter private(set) var downloadsPopover: DownloadsPopover? -#if NETWORK_PROTECTION private let networkProtectionPopoverManager: NetPPopoverManager init(networkProtectionPopoverManager: NetPPopoverManager, autofillPopoverPresenter: AutofillPopoverPresenter) { @@ -71,12 +65,6 @@ final class NavigationBarPopovers: PopoverPresenter { self.autofillPopoverPresenter = autofillPopoverPresenter } -#else - init(passwordPopoverPresenter: PasswordPopoverPresenter) { - self.passwordPopoverPresenter = passwordPopoverPresenter - } -#endif - var passwordManagementDomain: String? { didSet { autofillPopoverPresenter.passwordDomain = passwordManagementDomain @@ -101,11 +89,7 @@ final class NavigationBarPopovers: PopoverPresenter { @MainActor var isNetworkProtectionPopoverShown: Bool { -#if NETWORK_PROTECTION networkProtectionPopoverManager.isShown -#else - return false -#endif } var bookmarkListPopoverShown: Bool { @@ -129,9 +113,7 @@ final class NavigationBarPopovers: PopoverPresenter { } func toggleNetworkProtectionPopover(usingView view: NSView, withDelegate delegate: NSPopoverDelegate) { -#if NETWORK_PROTECTION networkProtectionPopoverManager.toggle(positionedBelow: view, withDelegate: delegate) -#endif } func toggleDownloadsPopover(usingView view: NSView, popoverDelegate: NSPopoverDelegate, downloadsDelegate: DownloadsViewControllerDelegate) { @@ -189,11 +171,9 @@ final class NavigationBarPopovers: PopoverPresenter { downloadsPopover?.close() } -#if NETWORK_PROTECTION if networkProtectionPopoverManager.isShown { networkProtectionPopoverManager.close() } -#endif return true } @@ -297,13 +277,11 @@ final class NavigationBarPopovers: PopoverPresenter { // MARK: - VPN -#if NETWORK_PROTECTION func showNetworkProtectionPopover( positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { networkProtectionPopoverManager.show(positionedBelow: view, withDelegate: delegate) } -#endif } extension Notification.Name { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 011aa09845..edd999e760 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -20,12 +20,9 @@ import Cocoa import Combine import Common import BrowserServicesKit - -#if NETWORK_PROTECTION import NetworkProtection import NetworkProtectionIPC import NetworkProtectionUI -#endif #if SUBSCRIPTION import Subscription @@ -107,12 +104,9 @@ final class NavigationBarViewController: NSViewController { static private let homeButtonTag = 3 static private let homeButtonLeftPosition = 0 -#if NETWORK_PROTECTION private let networkProtectionButtonModel: NetworkProtectionNavBarButtonModel private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation -#endif -#if NETWORK_PROTECTION static func create(tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), downloadListCoordinator: DownloadListCoordinator = .shared, @@ -136,24 +130,6 @@ final class NavigationBarViewController: NSViewController { goForwardButtonMenuDelegate = NavigationButtonMenuDelegate(buttonType: .forward, tabCollectionViewModel: tabCollectionViewModel) super.init(coder: coder) } -#else - static func create(tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, downloadListCoordinator: DownloadListCoordinator = .shared, autofillPopoverPresenter: AutofillPopoverPresenter) -> NavigationBarViewController { - NSStoryboard(name: "NavigationBar", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, downloadListCoordinator: downloadListCoordinator, passwordPopoverPresenter: passwordPopoverPresenter) - }! - } - - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, downloadListCoordinator: DownloadListCoordinator, - autofillPopoverPresenter: AutofillPopoverPresenter) { - self.popovers = NavigationBarPopovers(autofillPopoverPresenter: autofillPopoverPresenter) - self.tabCollectionViewModel = tabCollectionViewModel - self.isBurner = isBurner - self.downloadListCoordinator = downloadListCoordinator - goBackButtonMenuDelegate = NavigationButtonMenuDelegate(buttonType: .back, tabCollectionViewModel: tabCollectionViewModel) - goForwardButtonMenuDelegate = NavigationButtonMenuDelegate(buttonType: .forward, tabCollectionViewModel: tabCollectionViewModel) - super.init(coder: coder) - } -#endif required init?(coder: NSCoder) { fatalError("NavigationBarViewController: Bad initializer") @@ -169,9 +145,7 @@ final class NavigationBarViewController: NSViewController { setupNavigationButtonMenus() subscribeToSelectedTabViewModel() -#if NETWORK_PROTECTION listenToVPNToggleNotifications() -#endif listenToPasswordManagerNotifications() listenToPinningManagerNotifications() listenToMessageNotifications() @@ -189,9 +163,7 @@ final class NavigationBarViewController: NSViewController { networkProtectionButton.toolTip = UserText.networkProtectionButtonTooltip -#if NETWORK_PROTECTION setupNetworkProtectionButton() -#endif #if DEBUG || REVIEW addDebugNotificationListeners() @@ -313,7 +285,6 @@ final class NavigationBarViewController: NSViewController { popovers.passwordManagementButtonPressed(usingView: passwordManagementButton, withDelegate: self) } -#if NETWORK_PROTECTION @IBAction func networkProtectionButtonAction(_ sender: NSButton) { toggleNetworkProtectionPopover() } @@ -350,7 +321,6 @@ final class NavigationBarViewController: NSViewController { NetworkProtectionWaitlistViewControllerPresenter.show() } } -#endif @IBAction func downloadsButtonAction(_ sender: NSButton) { toggleDownloadsPopover(keepButtonVisible: false) @@ -365,7 +335,6 @@ final class NavigationBarViewController: NSViewController { super.mouseDown(with: event) } -#if NETWORK_PROTECTION func listenToVPNToggleNotifications() { vpnToggleCancellable = NotificationCenter.default.publisher(for: .ToggleNetworkProtectionInMainWindow).receive(on: DispatchQueue.main).sink { [weak self] _ in guard self?.view.window?.isKeyWindow == true else { @@ -375,7 +344,6 @@ final class NavigationBarViewController: NSViewController { self?.toggleNetworkProtectionPopover() } } -#endif func listenToPasswordManagerNotifications() { passwordManagerNotificationCancellable = NotificationCenter.default.publisher(for: .PasswordManagerChanged).sink { [weak self] _ in @@ -401,10 +369,8 @@ final class NavigationBarViewController: NSViewController { self.updateDownloadsButton(updatingFromPinnedViewsNotification: true) case .homeButton: self.updateHomeButton() -#if NETWORK_PROTECTION case .networkProtection: self.networkProtectionButtonModel.updateVisibility() -#endif } } else { assertionFailure("Failed to get changed pinned view type") @@ -435,7 +401,6 @@ final class NavigationBarViewController: NSViewController { name: AutoconsentUserScript.newSitePopupHiddenNotification, object: nil) -#if NETWORK_PROTECTION UserDefaults.netP .publisher(for: \.networkProtectionShouldShowVPNUninstalledMessage) .receive(on: DispatchQueue.main) @@ -446,7 +411,6 @@ final class NavigationBarViewController: NSViewController { } } .store(in: &cancellables) -#endif } @objc private func showVPNUninstalledFeedback() { @@ -940,14 +904,12 @@ extension NavigationBarViewController: NSMenuDelegate { let downloadsTitle = LocalPinningManager.shared.shortcutTitle(for: .downloads) menu.addItem(withTitle: downloadsTitle, action: #selector(toggleDownloadsPanelPinning), keyEquivalent: "J") -#if NETWORK_PROTECTION let isPopUpWindow = view.window?.isPopUpWindow ?? false if !isPopUpWindow && DefaultNetworkProtectionVisibility().isVPNVisible() { let networkProtectionTitle = LocalPinningManager.shared.shortcutTitle(for: .networkProtection) menu.addItem(withTitle: networkProtectionTitle, action: #selector(toggleNetworkProtectionPanelPinning), keyEquivalent: "N") } -#endif } @objc @@ -967,14 +929,11 @@ extension NavigationBarViewController: NSMenuDelegate { @objc private func toggleNetworkProtectionPanelPinning(_ sender: NSMenuItem) { -#if NETWORK_PROTECTION LocalPinningManager.shared.togglePinning(for: .networkProtection) -#endif } // MARK: - VPN -#if NETWORK_PROTECTION func showNetworkProtectionStatus() { let featureVisibility = DefaultNetworkProtectionVisibility() @@ -1019,7 +978,6 @@ extension NavigationBarViewController: NSMenuDelegate { } .store(in: &cancellables) } -#endif } @@ -1066,11 +1024,7 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } func optionsButtonMenuRequestedNetworkProtectionPopover(_ menu: NSMenu) { -#if NETWORK_PROTECTION toggleNetworkProtectionPopover() -#else - fatalError("Tried to open the VPN when it was disabled") -#endif } func optionsButtonMenuRequestedDownloadsPopover(_ menu: NSMenu) { @@ -1178,8 +1132,6 @@ extension NavigationBarViewController { } #endif -#if NETWORK_PROTECTION extension Notification.Name { static let ToggleNetworkProtectionInMainWindow = Notification.Name("com.duckduckgo.vpn.toggle-popover-in-main-window") } -#endif diff --git a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift index ef82b84da6..0af23bdcd5 100644 --- a/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift +++ b/DuckDuckGo/NavigationBar/View/NetPPopoverManagerMock.swift @@ -16,7 +16,7 @@ // limitations under the License. // -#if DEBUG && NETWORK_PROTECTION +#if DEBUG import Combine import Foundation diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift index b85ed68d8b..6aecbc6727 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if (NETWORK_PROTECTION || NETP_SYSTEM_EXTENSION) - import AppKit import Foundation import Common @@ -156,5 +154,3 @@ extension URL { } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index efa517356a..890f942b93 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import PixelKit import NetworkProtection @@ -415,5 +413,3 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { } } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionShared.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionShared.swift index 5e0799f8d5..e079676ce8 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionShared.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/UserDefaults+NetworkProtectionShared.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Combine import Foundation import NetworkProtectionUI @@ -77,5 +75,3 @@ extension NetworkProtectionUI.OnboardingStatus { #endif }() } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionOptionKeyExtension.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionOptionKeyExtension.swift index db40592ece..5c87d93191 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionOptionKeyExtension.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionTargets/AppAndExtensionAndNotificationTargets/NetworkProtectionOptionKeyExtension.swift @@ -16,12 +16,8 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import NetworkProtection extension NetworkProtectionOptionKey { public static let defaultPixelHeaders = "defaultPixelHeaders" } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift index 641c69c7f5..ed515e70e7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Common import Foundation import NetworkProtection @@ -100,5 +98,3 @@ extension EventMapping where Event == NetworkProtectionError { PixelKit.fire(debugEvent, frequency: .standard, includeAppVersionParameter: true) } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift index 7307d53480..9329924b85 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteCodeViewModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Combine import NetworkProtection import SwiftUIExtensions @@ -184,5 +182,3 @@ final class NetworkProtectionInviteSuccessViewModel: InviteCodeSuccessViewModel } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift index 64e3aa7c3b..2d6aa8476e 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import SwiftUI import NetworkProtection import SwiftUIExtensions @@ -36,5 +34,3 @@ struct NetworkProtectionInviteDialog: View { } } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift index f80e5eb093..d845eedc0c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInvitePresenter.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import SwiftUI import NetworkProtection @@ -64,5 +62,3 @@ final class NetworkProtectionInvitePresenter: NetworkProtectionInvitePresenting, } } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift index d08a236990..30d99488bf 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import LoginItems @@ -38,5 +36,3 @@ extension LoginItemsManager { return items } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 0beb6527cd..79efd7d30a 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import NetworkProtection import NetworkProtectionIPC @@ -95,5 +93,3 @@ extension TunnelControllerIPCClient { self.init(machServiceName: Bundle.main.vpnMenuAgentBundleId) } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index 8830942459..89aa046691 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -16,7 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION import Common import Foundation import LoginItems @@ -118,5 +117,3 @@ final class NetworkProtectionAppEvents { } } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionControllerErrorStore.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionControllerErrorStore.swift index 62503023be..a5c6110338 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionControllerErrorStore.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionControllerErrorStore.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import NetworkProtection @@ -54,5 +52,3 @@ final class NetworkProtectionControllerErrorStore { distributedNotificationCenter.post(.controllerErrorChanged, object: errorMessage) } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 9495954da1..2833f1a23d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import Common import Foundation @@ -668,5 +666,3 @@ extension NetworkProtectionDebugMenu: NSMenuDelegate { return MenuPreview(menu: NetworkProtectionDebugMenu()) } #endif - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index fe66a59959..8c141f7061 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -18,8 +18,6 @@ import Common import Foundation - -#if NETWORK_PROTECTION import NetworkProtection import NetworkProtectionUI import NetworkExtension @@ -87,5 +85,3 @@ final class NetworkProtectionDebugUtilities { try await ipcClient.debugCommand(.expireRegistrationKey) } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index cd6d4900a5..d8d1aaedf2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import Combine import Foundation @@ -222,5 +220,3 @@ extension NetworkProtectionNavBarButtonModel: NSPopoverDelegate { updateVisibility() } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 089a82f3f1..7bb6fa1363 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -28,8 +28,6 @@ import NetworkProtectionUI import Subscription #endif -#if NETWORK_PROTECTION - protocol NetworkProtectionIPCClient { var ipcStatusObserver: ConnectionStatusObserver { get } var ipcServerInfoObserver: ConnectionServerInfoObserver { get } @@ -57,11 +55,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { } var isShown: Bool { -#if NETWORK_PROTECTION networkProtectionPopover?.isShown ?? false -#else - return false -#endif } // swiftlint:disable:next function_body_length @@ -156,4 +150,3 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { networkProtectionPopover?.close() } } -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift index d9ca618427..7936e3ec09 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import Foundation import NetworkProtection @@ -81,5 +79,3 @@ final class NetworkProtectionOnboardingMenu: NSMenu { return MenuPreview(menu: NetworkProtectionOnboardingMenu()) } #endif - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionSimulateFailureMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionSimulateFailureMenu.swift index fa62d0ae6a..2f4fb420ee 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionSimulateFailureMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionSimulateFailureMenu.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import Foundation import NetworkProtection @@ -98,5 +96,3 @@ final class NetworkProtectionSimulateFailureMenu: NSMenu { simulateConnectionInterruptionMenuItem.state = simulationOptions.isEnabled(.connectionInterruption) ? .on : .off } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 95764249c9..e42b917710 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import Combine import SwiftUI @@ -764,5 +762,3 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr "ddg:\(token)" } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift index dd0783b9c9..aa9fce2460 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionWaitlistFeatureFlagOverridesMenu.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import Foundation import NetworkProtection @@ -166,5 +164,3 @@ final class NetworkProtectionWaitlistFeatureFlagOverridesMenu: NSMenu { return MenuPreview(menu: NetworkProtectionWaitlistFeatureFlagOverridesMenu()) } #endif - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift index 12bfce37da..28c6a16b19 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/NetworkProtectionVPNCountryLabelsModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import NetworkProtection @@ -41,5 +39,3 @@ struct NetworkProtectionVPNCountryLabelsModel { return flag } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift index 17750d602b..8c898abbd7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import SwiftUI @@ -63,5 +61,3 @@ struct VPNLocationPreferenceItem: View { } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift index dc8bda6885..c68ccc4154 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItemModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import NetworkProtection @@ -48,5 +46,3 @@ final class VPNLocationPreferenceItemModel: ObservableObject { } } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift index 15e6bcdc66..36fd86779a 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import PreferencesViews import SwiftUI import SwiftUIExtensions @@ -285,5 +283,3 @@ private struct VPNLocationViewButtons: View { } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index 24dfd9c4ab..01a39e35f4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import Combine import NetworkProtection @@ -208,5 +206,3 @@ private extension String { Locale.current.localizedString(forRegionCode: self) ?? "" } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationsHostingViewController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationsHostingViewController.swift index cf3d79d345..d678b0b9c0 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationsHostingViewController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationsHostingViewController.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import SwiftUI @@ -44,5 +42,3 @@ final class VPNLocationsHostingViewController: NSHostingController Void)?) @@ -178,5 +176,3 @@ final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMess } } - -#endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 6a9cc26537..78015982d6 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -16,7 +16,7 @@ // limitations under the License. // -#if NETWORK_PROTECTION && SUBSCRIPTION +#if SUBSCRIPTION import Combine import Foundation diff --git a/DuckDuckGo/Preferences/Model/AboutModel.swift b/DuckDuckGo/Preferences/Model/AboutModel.swift index 52508bd9cb..1504240116 100644 --- a/DuckDuckGo/Preferences/Model/AboutModel.swift +++ b/DuckDuckGo/Preferences/Model/AboutModel.swift @@ -22,17 +22,11 @@ import Common final class AboutModel: ObservableObject, PreferencesTabOpening { let appVersion = AppVersion() -#if NETWORK_PROTECTION private let netPInvitePresenter: NetworkProtectionInvitePresenting -#endif -#if NETWORK_PROTECTION init(netPInvitePresenter: NetworkProtectionInvitePresenting) { self.netPInvitePresenter = netPInvitePresenter } -#else - init() {} -#endif let displayableAboutURL: String = URL.aboutDuckDuckGo .toString(decodePunycode: false, dropScheme: true, dropTrailingSlash: false) @@ -46,9 +40,7 @@ final class AboutModel: ObservableObject, PreferencesTabOpening { NSPasteboard.general.copy(value) } -#if NETWORK_PROTECTION func displayNetPInvite() { netPInvitePresenter.present() } -#endif } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index d1d1a8ebfd..161ffb3440 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -31,11 +31,9 @@ struct PreferencesSection: Hashable, Identifiable { static func defaultSections(includingDuckPlayer: Bool, includingSync: Bool, includingVPN: Bool) -> [PreferencesSection] { var privacyPanes: [PreferencePaneIdentifier] = [.defaultBrowser, .privateSearch, .webTrackingProtection, .cookiePopupProtection, .emailProtection] -#if NETWORK_PROTECTION if includingVPN { privacyPanes.append(.vpn) } -#endif let regularPanes: [PreferencePaneIdentifier] = { var panes: [PreferencePaneIdentifier] = [.general, .appearance, .autofill, .accessibility, .dataClearing] @@ -111,9 +109,7 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { case sync case appearance case dataClearing -#if NETWORK_PROTECTION case vpn -#endif #if SUBSCRIPTION case subscription #endif @@ -168,10 +164,8 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { return UserText.appearance case .dataClearing: return UserText.dataClearing -#if NETWORK_PROTECTION case .vpn: return UserText.vpn -#endif #if SUBSCRIPTION case .subscription: return UserText.subscription @@ -209,10 +203,8 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { return "Appearance" case .dataClearing: return "FireSettings" -#if NETWORK_PROTECTION case .vpn: return "VPN" -#endif #if SUBSCRIPTION case .subscription: return "PrivacyPro" diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index b5ba825f14..3cb7adc0a7 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -69,9 +69,7 @@ final class PreferencesSidebarModel: ObservableObject { } .store(in: &cancellables) -#if NETWORK_PROTECTION setupVPNPaneVisibility() -#endif } @MainActor @@ -83,11 +81,7 @@ final class PreferencesSidebarModel: ObservableObject { userDefaults: UserDefaults = .netP ) { let loadSections = { -#if NETWORK_PROTECTION let includingVPN = DefaultNetworkProtectionVisibility().isInstalled -#else - let includingVPN = false -#endif return PreferencesSection.defaultSections( includingDuckPlayer: includeDuckPlayer, @@ -104,7 +98,6 @@ final class PreferencesSidebarModel: ObservableObject { // MARK: - Setup -#if NETWORK_PROTECTION private func setupVPNPaneVisibility() { DefaultNetworkProtectionVisibility().onboardStatusPublisher .receive(on: DispatchQueue.main) @@ -115,7 +108,6 @@ final class PreferencesSidebarModel: ObservableObject { } .store(in: &cancellables) } -#endif // MARK: - Refreshing logic diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 54ead02220..320d0d2cad 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import Combine import Foundation @@ -134,5 +132,3 @@ final class VPNPreferencesModel: ObservableObject { return alert } } - -#endif diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift index d298ef89fd..63c0601ea8 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift @@ -56,9 +56,7 @@ extension Preferences { Text(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber)) .onTapGesture(count: 12) { -#if NETWORK_PROTECTION model.displayNetPInvite() -#endif } .contextMenu(ContextMenu(menuItems: { Button(UserText.copy, action: { diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index d550717a5f..1dde6f4bbf 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -101,11 +101,8 @@ enum Preferences { AppearanceView(model: .shared) case .dataClearing: DataClearingView(model: DataClearingPreferences.shared) - -#if NETWORK_PROTECTION case .vpn: VPNView(model: VPNPreferencesModel()) -#endif #if SUBSCRIPTION case .subscription: @@ -121,12 +118,8 @@ enum Preferences { // Opens a new tab Spacer() case .about: -#if NETWORK_PROTECTION let netPInvitePresenter = NetworkProtectionInvitePresenter() AboutView(model: AboutModel(netPInvitePresenter: netPInvitePresenter)) -#else - AboutView(model: AboutModel()) -#endif } } .frame(maxWidth: Const.paneContentWidth, maxHeight: .infinity, alignment: .topLeading) diff --git a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift index 2485bbb6c5..5cce49df31 100644 --- a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import PreferencesViews import SwiftUI import SwiftUIExtensions @@ -105,5 +103,3 @@ extension Preferences { } } } - -#endif diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index fa20ea57c6..37d63c320f 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -21,10 +21,7 @@ import SwiftUI import SwiftUIExtensions import Combine import DDGSync - -#if NETWORK_PROTECTION import NetworkProtection -#endif final class PreferencesViewController: NSViewController { diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 6fd7bffd3a..643da5aa94 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -26,16 +26,13 @@ import UserScript import WebKit import History import PrivacyDashboard +import NetworkProtection +import NetworkProtectionIPC #if SUBSCRIPTION import Subscription #endif -#if NETWORK_PROTECTION -import NetworkProtection -import NetworkProtectionIPC -#endif - // swiftlint:disable file_length protocol TabDelegate: ContentOverlayUserScriptDelegate { @@ -344,9 +341,7 @@ protocol NewWindowPolicyDecisionMaker { private let internalUserDecider: InternalUserDecider? let pinnedTabsManager: PinnedTabsManager -#if NETWORK_PROTECTION private(set) var tunnelController: NetworkProtectionIPCTunnelController? -#endif private let webViewConfiguration: WKWebViewConfiguration @@ -534,7 +529,6 @@ protocol NewWindowPolicyDecisionMaker { self?.onDuckDuckGoEmailSignOut(notification) } -#if NETWORK_PROTECTION netPOnboardStatusCancellabel = DefaultNetworkProtectionVisibility().onboardStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] onboardingStatus in @@ -545,7 +539,6 @@ protocol NewWindowPolicyDecisionMaker { self?.tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) } -#endif self.audioState = webView.audioState() addDeallocationChecks(for: webView) @@ -1177,9 +1170,7 @@ protocol NewWindowPolicyDecisionMaker { private var webViewCancellables = Set() private var emailDidSignOutCancellable: AnyCancellable? -#if NETWORK_PROTECTION private var netPOnboardStatusCancellabel: AnyCancellable? -#endif private func setupWebView(shouldLoadInBackground: Bool) { webView.navigationDelegate = navigationDelegate @@ -1465,11 +1456,9 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift } } -#if NETWORK_PROTECTION if navigation.url.isDuckDuckGoSearch, tunnelController?.isConnected == true { DailyPixel.fire(pixel: .networkProtectionEnabledOnSearch, frequency: .dailyAndCount) } -#endif } @MainActor diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackCategory.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackCategory.swift index 34adf335df..972f6ef4e7 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackCategory.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackCategory.swift @@ -18,8 +18,6 @@ import Foundation -#if NETWORK_PROTECTION - enum VPNFeedbackCategory: String, CaseIterable { case landingPage case unableToInstall @@ -61,5 +59,3 @@ enum VPNFeedbackCategory: String, CaseIterable { } } } - -#endif diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift index 17323bbedc..fb3f6ccf4b 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift @@ -19,8 +19,6 @@ import Foundation import SwiftUI -#if NETWORK_PROTECTION - struct VPNFeedbackFormView: View { @EnvironmentObject var viewModel: VPNFeedbackFormViewModel @@ -214,5 +212,3 @@ private struct VPNFeedbackFormButtons: View { } } - -#endif diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift index 4311945264..fe8b8258e2 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import AppKit import SwiftUI @@ -120,5 +118,3 @@ extension VPNFeedbackFormViewController: VPNFeedbackFormViewModelDelegate { } } - -#endif diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift index 76282a51af..7363d0d817 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import Combine import SwiftUI @@ -100,5 +98,3 @@ final class VPNFeedbackFormViewModel: ObservableObject { } } - -#endif diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackSender.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackSender.swift index 52a951acbd..e799bbea54 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackSender.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackSender.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation protocol VPNFeedbackSender { @@ -43,5 +41,3 @@ struct DefaultVPNFeedbackSender: VPNFeedbackSender { } } - -#endif diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index f724c14aae..c24d64bf6e 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import AppKit import Common @@ -285,5 +283,3 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } } - -#endif diff --git a/DuckDuckGo/Waitlist/Models/WaitlistViewModel.swift b/DuckDuckGo/Waitlist/Models/WaitlistViewModel.swift index 5be2d26fe1..6419bb86c5 100644 --- a/DuckDuckGo/Waitlist/Models/WaitlistViewModel.swift +++ b/DuckDuckGo/Waitlist/Models/WaitlistViewModel.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import NetworkProtection import UserNotifications @@ -232,5 +230,3 @@ final class WaitlistViewModel: ObservableObject { termsAndConditionActionHandler.didAccept() } } - -#endif diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index ba12c9cdd2..3291b3867c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import BrowserServicesKit import Common import NetworkExtension @@ -165,5 +163,3 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling } } } - -#endif diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 96341cc2db..72d787249c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import BrowserServicesKit import Combine import Common @@ -261,5 +259,3 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { isPreSubscriptionUser() && subscriptionFeatureAvailability.isFeatureAvailable } } - -#endif diff --git a/DuckDuckGo/Waitlist/Views/WaitlistDialogView.swift b/DuckDuckGo/Waitlist/Views/WaitlistDialogView.swift index 70f22ec3ea..11e5fe8622 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistDialogView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistDialogView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import SwiftUI struct WaitlistDialogView: View where Content: View, Buttons: View { @@ -64,5 +62,3 @@ struct WaitlistDialogView: View where Content: View, Buttons: // .padding(.bottom, 16.0) } } - -#endif diff --git a/DuckDuckGo/Waitlist/Views/WaitlistModalViewController.swift b/DuckDuckGo/Waitlist/Views/WaitlistModalViewController.swift index ac7f7a1f58..80a977d6ae 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistModalViewController.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistModalViewController.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import AppKit import SwiftUI import UserNotifications @@ -107,5 +105,3 @@ struct WaitlistModalDismisser { } } } - -#endif diff --git a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift index d4cd80abd5..776e44acdf 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistRootView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import SwiftUI struct NetworkProtectionWaitlistRootView: View { @@ -45,8 +43,6 @@ struct NetworkProtectionWaitlistRootView: View { } } -#endif - #if DBP import SwiftUI diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift index 074c74d1ca..1f4ee5d72c 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/EnableWaitlistFeatureView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION || DBP - import SwiftUI import SwiftUIExtensions @@ -62,10 +60,6 @@ struct EnableWaitlistFeatureView: View { } } -#endif - -#if NETWORK_PROTECTION - struct EnableNetworkProtectionViewData: EnableWaitlistFeatureViewData { var headerImageName: String = "Network-Protection-256" var title: String = UserText.networkProtectionWaitlistEnableTitle @@ -73,5 +67,3 @@ struct EnableNetworkProtectionViewData: EnableWaitlistFeatureViewData { var availabilityDisclaimer: String = UserText.networkProtectionWaitlistAvailabilityDisclaimer var buttonConfirmLabel: String = UserText.networkProtectionWaitlistButtonGotIt } - -#endif diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift index dc4b75749c..727156f946 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/InvitedToWaitlistView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import SwiftUI import SwiftUIExtensions @@ -119,10 +117,6 @@ struct WaitlistEntryViewItemViewData: Identifiable { let subtitle: String } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { let headerImageName = "Gift-96" let title = UserText.networkProtectionWaitlistInvitedTitle @@ -146,8 +140,6 @@ struct NetworkProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { ] } -#endif - #if DBP struct DataBrokerProtectionInvitedToWaitlistViewData: InvitedToWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift index 47aaf86705..b47869f981 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinWaitlistView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION || DBP - import SwiftUI import SwiftUIExtensions @@ -75,10 +73,6 @@ struct JoinWaitlistView: View { } } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { let headerImageName = "JoinWaitlistHeader" let title = UserText.networkProtectionWaitlistJoinTitle @@ -89,8 +83,6 @@ struct NetworkProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { let buttonJoinWaitlistLabel = UserText.networkProtectionWaitlistButtonJoinWaitlist } -#endif - #if DBP struct DataBrokerProtectionJoinWaitlistViewData: JoinWaitlistViewViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift index bcf30d95ad..bda0183f51 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/JoinedWaitlistView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION || DBP - import SwiftUI import SwiftUIExtensions @@ -87,10 +85,6 @@ struct JoinedWaitlistView: View { } } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { let headerImageName = "JoinedWaitlistHeader" var title = UserText.networkProtectionWaitlistJoinedTitle @@ -102,8 +96,6 @@ struct NetworkProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { var buttonEnableNotificationLabel = UserText.networkProtectionWaitlistButtonEnableNotifications } -#endif - #if DBP struct DataBrokerProtectionJoinedWaitlistViewData: JoinedWaitlistViewData { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift index a10b10e34c..0137b172e8 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistSteps/WaitlistTermsAndConditionsView.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION || DBP - import SwiftUI import SwiftUIExtensions @@ -84,10 +82,6 @@ private extension Text { } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionTermsAndConditionsContentView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -158,8 +152,6 @@ struct NetworkProtectionWaitlistTermsAndConditionsViewData: WaitlistTermsAndCond let buttonAgreeAndContinueLabel = UserText.networkProtectionWaitlistButtonAgreeAndContinue } -#endif - #if DBP struct DataBrokerProtectionTermsAndConditionsContentView: View { diff --git a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift index 22fcf9bbdb..7cded8cb81 100644 --- a/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift +++ b/DuckDuckGo/Waitlist/Views/WaitlistViewControllerPresenter.swift @@ -20,8 +20,6 @@ import Foundation import UserNotifications import Subscription -#if NETWORK_PROTECTION || DBP - protocol WaitlistViewControllerPresenter { static func show(completion: (() -> Void)?) } @@ -32,10 +30,6 @@ extension WaitlistViewControllerPresenter { } } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { @MainActor @@ -75,8 +69,6 @@ struct NetworkProtectionWaitlistViewControllerPresenter: WaitlistViewControllerP } } -#endif - #if DBP struct DataBrokerProtectionWaitlistViewControllerPresenter: WaitlistViewControllerPresenter { diff --git a/DuckDuckGo/Waitlist/Waitlist.swift b/DuckDuckGo/Waitlist/Waitlist.swift index 30b211e0ec..5a28b3f257 100644 --- a/DuckDuckGo/Waitlist/Waitlist.swift +++ b/DuckDuckGo/Waitlist/Waitlist.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import Networking import UserNotifications @@ -238,8 +236,6 @@ struct NetworkProtectionWaitlist: Waitlist { } -#endif - #if DBP // MARK: - DataBroker Protection Waitlist diff --git a/DuckDuckGo/Waitlist/WaitlistFeatureSetupHandler.swift b/DuckDuckGo/Waitlist/WaitlistFeatureSetupHandler.swift index 587acfda5e..6c2c63b563 100644 --- a/DuckDuckGo/Waitlist/WaitlistFeatureSetupHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistFeatureSetupHandler.swift @@ -16,18 +16,12 @@ // limitations under the License. // -#if NETWORK_PROTECTION || DBP - import Foundation protocol WaitlistFeatureSetupHandler { func confirmFeature() } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionWaitlistFeatureSetupHandler: WaitlistFeatureSetupHandler { func confirmFeature() { LocalPinningManager.shared.pin(.networkProtection) @@ -35,8 +29,6 @@ struct NetworkProtectionWaitlistFeatureSetupHandler: WaitlistFeatureSetupHandler } } -#endif - #if DBP struct DataBrokerProtectionWaitlistFeatureSetupHandler: WaitlistFeatureSetupHandler { diff --git a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift index 449ada2a92..c0bb48b726 100644 --- a/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift +++ b/DuckDuckGo/Waitlist/WaitlistTermsAndConditionsActionHandler.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION || DBP - import Foundation import UserNotifications @@ -27,10 +25,6 @@ protocol WaitlistTermsAndConditionsActionHandler { mutating func didAccept() } -#endif - -#if NETWORK_PROTECTION - struct NetworkProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { @UserDefaultsWrapper(key: .networkProtectionTermsAndConditionsAccepted, defaultValue: false) var acceptedTermsAndConditions: Bool @@ -46,8 +40,6 @@ struct NetworkProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAn } } -#endif - #if DBP struct DataBrokerProtectionWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsActionHandler { diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 92a871a208..87cdf8fad5 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -200,7 +200,6 @@ extension WindowControllersManager { // MARK: - VPN -#if NETWORK_PROTECTION @MainActor func showNetworkProtectionStatus(retry: Bool = false) async { guard let windowController = mainWindowControllers.first else { @@ -244,7 +243,6 @@ extension WindowControllersManager { parentWindowController.window?.beginSheet(locationsFormWindow) } -#endif } diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 08f034af48..f2b948f355 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -20,8 +20,6 @@ import XCTest import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser -#if NETWORK_PROTECTION - final class MockNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMessaging { var messages: [NetworkProtectionRemoteMessage] = [] @@ -38,8 +36,6 @@ final class MockNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMessagi } -#endif - #if DBP final class MockDataBrokerProtectionRemoteMessaging: DataBrokerProtectionRemoteMessaging { @@ -96,25 +92,18 @@ final class ContinueSetUpModelTests: XCTestCase { privacyConfigManager.privacyConfig = config randomNumberGenerator = MockRandomNumberGenerator() -#if NETWORK_PROTECTION && DBP +#if DBP let messaging = HomePageRemoteMessaging( networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: userDefaults, dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: userDefaults ) -#elseif NETWORK_PROTECTION +#else let messaging = HomePageRemoteMessaging( networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: userDefaults ) -#elseif DBP - let messaging = HomePageRemoteMessaging( - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: userDefaults - ) -#else - let messaging = HomePageRemoteMessaging.defaultMessaging() #endif vm = HomePage.Models.ContinueSetUpModel( @@ -571,25 +560,18 @@ final class ContinueSetUpModelTests: XCTestCase { } private func createMessaging() -> HomePageRemoteMessaging { -#if NETWORK_PROTECTION && DBP +#if DBP return HomePageRemoteMessaging( networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: userDefaults, dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: userDefaults ) -#elseif NETWORK_PROTECTION +#else return HomePageRemoteMessaging( networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: userDefaults ) -#elseif DBP - return HomePageRemoteMessaging( - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: userDefaults - ) -#else - return HomePageRemoteMessaging.defaultMessaging() #endif } @@ -617,25 +599,18 @@ extension HomePage.Models.ContinueSetUpModel { let manager = MockPrivacyConfigurationManager() manager.privacyConfig = privacyConfig -#if NETWORK_PROTECTION && DBP +#if DBP let messaging = HomePageRemoteMessaging( networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: appGroupUserDefaults, dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), dataBrokerProtectionUserDefaults: appGroupUserDefaults ) -#elseif NETWORK_PROTECTION +#else let messaging = HomePageRemoteMessaging( networkProtectionRemoteMessaging: MockNetworkProtectionRemoteMessaging(), networkProtectionUserDefaults: appGroupUserDefaults ) -#elseif DBP - let messaging = HomePageRemoteMessaging( - dataBrokerProtectionRemoteMessaging: MockDataBrokerProtectionRemoteMessaging(), - dataBrokerProtectionUserDefaults: appGroupUserDefaults - ) -#else - let messaging = HomePageRemoteMessaging.defaultMessaging() #endif return HomePage.Models.ContinueSetUpModel( diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 95fe1e0739..1aec539d0c 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -22,9 +22,7 @@ import XCTest import Subscription #endif -#if NETWORK_PROTECTION import NetworkProtection -#endif @testable import DuckDuckGo_Privacy_Browser @@ -35,27 +33,18 @@ final class MoreOptionsMenuTests: XCTestCase { var capturingActionDelegate: CapturingOptionsButtonMenuDelegate! @MainActor lazy var moreOptionMenu: MoreOptionsMenu! = { -#if NETWORK_PROTECTION let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: networkProtectionVisibilityMock, sharingMenu: NSMenu(), internalUserDecider: internalUserDecider) -#else - let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) -#endif menu.actionDelegate = capturingActionDelegate return menu }() var internalUserDecider: InternalUserDeciderMock! -#if NETWORK_PROTECTION var networkProtectionVisibilityMock: NetworkProtectionVisibilityMock! -#endif @MainActor override func setUp() { @@ -65,9 +54,7 @@ final class MoreOptionsMenuTests: XCTestCase { capturingActionDelegate = CapturingOptionsButtonMenuDelegate() internalUserDecider = InternalUserDeciderMock() -#if NETWORK_PROTECTION networkProtectionVisibilityMock = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) -#endif } @MainActor @@ -81,18 +68,11 @@ final class MoreOptionsMenuTests: XCTestCase { @MainActor func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsEnabled() { -#if NETWORK_PROTECTION moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider) -#else - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) -#endif XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) @@ -108,7 +88,6 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(moreOptionMenu.items[11].isSeparatorItem) XCTAssertEqual(moreOptionMenu.items[12].title, UserText.emailOptionsMenuItem) -#if NETWORK_PROTECTION if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.networkProtection)) @@ -121,26 +100,15 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(moreOptionMenu.items[15].isSeparatorItem) XCTAssertEqual(moreOptionMenu.items[16].title, UserText.settings) } -#else - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[14].title, UserText.settings) -#endif } @MainActor func testThatMoreOptionMenuHasTheExpectedItems_WhenNetworkProtectionIsDisabled() { -#if NETWORK_PROTECTION moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: false), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider) -#else - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) -#endif XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) @@ -196,7 +164,6 @@ final class MoreOptionsMenuTests: XCTestCase { } -#if NETWORK_PROTECTION final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { var isInstalled: Bool @@ -239,4 +206,3 @@ final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility return false } } -#endif diff --git a/UnitTests/NavigationBar/LocalPinningManagerTests.swift b/UnitTests/NavigationBar/LocalPinningManagerTests.swift index 9c57966d62..5cf5cf7962 100644 --- a/UnitTests/NavigationBar/LocalPinningManagerTests.swift +++ b/UnitTests/NavigationBar/LocalPinningManagerTests.swift @@ -17,14 +17,10 @@ // import XCTest - -#if NETWORK_PROTECTION import NetworkProtection -#endif @testable import DuckDuckGo_Privacy_Browser -#if NETWORK_PROTECTION private struct NetworkProtectionFeatureActivationMock: NetworkProtectionFeatureActivation { let activated: Bool = true @@ -33,7 +29,6 @@ private struct NetworkProtectionFeatureActivationMock: NetworkProtectionFeatureA activated } } -#endif final class LocalPinningManagerTests: XCTestCase { @@ -48,11 +43,7 @@ final class LocalPinningManagerTests: XCTestCase { } private func createManager() -> LocalPinningManager { -#if NETWORK_PROTECTION return LocalPinningManager(networkProtectionFeatureActivation: NetworkProtectionFeatureActivationMock()) -#else - return LocalPinningManager() -#endif } func testWhenTogglingPinningForAView_AndViewIsNotPinned_ThenViewBecomesPinned() { diff --git a/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift b/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift index 461c0109be..091f73f4d9 100644 --- a/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift +++ b/UnitTests/NavigationBar/View/NavigationBarPopoversTests.swift @@ -29,12 +29,7 @@ final class NavigationBarPopoversTests: XCTestCase { override func setUpWithError() throws { autofillPopoverPresenter = MockAutofillPopoverPresenter() - - #if NETWORK_PROTECTION sut = NavigationBarPopovers(networkProtectionPopoverManager: NetPPopoverManagerMock(), autofillPopoverPresenter: autofillPopoverPresenter) - #else - sut = NavigationBarPopovers(passwordPopoverPresenter: popoverPresenter) - #endif } func testSetsPasswordPopoverDomainOnPopover() throws { diff --git a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift index 38483e97db..43ccdb22ab 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift +++ b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import NetworkProtection import PixelKit import PixelKitTestingUtilities @@ -321,5 +319,3 @@ final class NetworkProtectionPixelEventTests: XCTestCase { line: #line) } } - -#endif diff --git a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift b/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift index 761f184335..3f96a7d0ea 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift +++ b/UnitTests/NetworkProtection/NetworkProtectionRemoteMessagingTests.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import XCTest @testable import DuckDuckGo_Privacy_Browser @@ -376,5 +374,3 @@ final class MockWaitlistActivationDateStore: WaitlistActivationDateStore { } } - -#endif diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift index 5ad28ed209..f5f727456e 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift @@ -19,8 +19,6 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser -#if NETWORK_PROTECTION - final class VPNFeedbackFormViewModelTests: XCTestCase { func testWhenCreatingViewModel_ThenInitialStateIsFeedbackPending() throws { @@ -171,5 +169,3 @@ private class MockVPNFeedbackFormViewModelDelegate: VPNFeedbackFormViewModelDele } } - -#endif diff --git a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift b/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift index 629639fd4b..b998d135e0 100644 --- a/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift +++ b/UnitTests/Waitlist/Mocks/MockNetworkProtectionCodeRedeemer.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation import NetworkProtection @@ -48,5 +46,3 @@ final class MockNetworkProtectionCodeRedeemer: NetworkProtectionCodeRedeeming { } } - -#endif diff --git a/UnitTests/Waitlist/Mocks/MockWaitlistFeatureSetupHandler.swift b/UnitTests/Waitlist/Mocks/MockWaitlistFeatureSetupHandler.swift index 87c2e903f0..5e20b69faa 100644 --- a/UnitTests/Waitlist/Mocks/MockWaitlistFeatureSetupHandler.swift +++ b/UnitTests/Waitlist/Mocks/MockWaitlistFeatureSetupHandler.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation @testable import DuckDuckGo_Privacy_Browser @@ -26,5 +24,3 @@ struct MockWaitlistFeatureSetupHandler: WaitlistFeatureSetupHandler { } } - -#endif diff --git a/UnitTests/Waitlist/Mocks/MockWaitlistTermsAndConditionsActionHandler.swift b/UnitTests/Waitlist/Mocks/MockWaitlistTermsAndConditionsActionHandler.swift index e661f9a3de..5f0623646a 100644 --- a/UnitTests/Waitlist/Mocks/MockWaitlistTermsAndConditionsActionHandler.swift +++ b/UnitTests/Waitlist/Mocks/MockWaitlistTermsAndConditionsActionHandler.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import Foundation @testable import DuckDuckGo_Privacy_Browser @@ -32,5 +30,3 @@ struct MockWaitlistTermsAndConditionsActionHandler: WaitlistTermsAndConditionsAc } } - -#endif diff --git a/UnitTests/Waitlist/WaitlistViewModelTests.swift b/UnitTests/Waitlist/WaitlistViewModelTests.swift index 8c1319d92b..cfa6224650 100644 --- a/UnitTests/Waitlist/WaitlistViewModelTests.swift +++ b/UnitTests/Waitlist/WaitlistViewModelTests.swift @@ -16,8 +16,6 @@ // limitations under the License. // -#if NETWORK_PROTECTION - import XCTest @testable import DuckDuckGo_Privacy_Browser @@ -129,5 +127,3 @@ final class WaitlistViewModelTests: XCTestCase { } } - -#endif From 73d4a03d0334fcfaabacb8ee1225f01ddcb7a729 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Mon, 8 Apr 2024 17:47:53 +0200 Subject: [PATCH 025/221] Removes last instance of NETWORK_PROTECTION flag (#2573) Task/Issue URL: https://app.asana.com/0/0/1207025130407889/f ## Description Removes last instance of `NETWORK_PROTECTION`. --- DuckDuckGo/MainWindow/MainViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index d2442a68c2..5e0840ebc5 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -191,9 +191,7 @@ final class MainViewController: NSViewController { browserTabViewController.windowDidBecomeKey() presentWaitlistThankYouPromptIfNecessary() -#if NETWORK_PROTECTION refreshNetworkProtectionMessages() -#endif #if DBP DataBrokerProtectionAppEvents().windowDidBecomeMain() From 36844f9cbaf5f925d839a602bc148d7652b3940d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 8 Apr 2024 10:29:25 -0700 Subject: [PATCH 026/221] Improve VPN uninstallation reliability (#2560) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207011235017033/f Tech Design URL: CC: Description: This PR adds retry logic to the VPN uninstallation logic. --- .../Waitlist/NetworkProtectionFeatureDisabler.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index 3291b3867c..8e4b353be4 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -100,7 +100,18 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling } } - try? await removeVPNConfiguration() + var attemptNumber = 1 + while attemptNumber <= 3 { + do { + try await removeVPNConfiguration() + break // Removal succeeded, break out of the while loop and continue with the rest of uninstallation + } catch { + print("Failed to remove VPN configuration, with error: \(error.localizedDescription)") + } + + attemptNumber += 1 + } + // We want to give some time for the login item to reset state before disabling it try? await Task.sleep(interval: 0.5) disableLoginItems() From 612b50afe11009f033380f509fcb676ac8d8cfea Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 8 Apr 2024 12:55:44 -0700 Subject: [PATCH 027/221] Create a new window when making a feedback form if necessary (#2563) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207016420146353/f Tech Design URL: CC: Description: This PR makes sure that the VPN feedback form button always succeeds, even when there is no browser window available to display in. After the change, the VPN feedback option will open a new window if none was available. (We should probably have some cleaner way to do that, since get-or-create-window seems like a pretty common operation. Maybe there is and I missed it?) --- .../Windows/View/WindowControllersManager.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 87cdf8fad5..ae37e64c41 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -222,13 +222,19 @@ extension WindowControllersManager { let feedbackFormViewController = VPNFeedbackFormViewController() let feedbackFormWindowController = feedbackFormViewController.wrappedInWindowController() - guard let feedbackFormWindow = feedbackFormWindowController.window, - let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController else { - assertionFailure("Failed to present native VPN feedback form") + guard let feedbackFormWindow = feedbackFormWindowController.window else { + assertionFailure("Couldn't get window for feedback form") return } - parentWindowController.window?.beginSheet(feedbackFormWindow) + if let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController { + parentWindowController.window?.beginSheet(feedbackFormWindow) + } else { + let tabCollection = TabCollection(tabs: []) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + let window = WindowsManager.openNewWindow(with: tabCollectionViewModel) + window?.beginSheet(feedbackFormWindow) + } } func showLocationPickerSheet() { From c5cbe52b14410335661756b5cc121c407fde1a98 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 8 Apr 2024 13:18:27 -0700 Subject: [PATCH 028/221] Fix last known VPN crash and missing IPC registration (#2579) Task/Issue URL: https://app.asana.com/0/0/1207030461827911/f Tech Design URL: CC: Description: This PR fixes the last known VPN crash, and a missing registration. --- .../NetworkProtectionIPC/TunnelControllerIPCClient.swift | 2 ++ .../NetworkProtectionUI/NetworkProtectionIconPublisher.swift | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index d1b978f777..769939f65e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -86,6 +86,8 @@ public final class TunnelControllerIPCClient { self.register() } } + + self.register() } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionIconPublisher.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionIconPublisher.swift index c6c4527539..f886f92357 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionIconPublisher.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionIconPublisher.swift @@ -60,13 +60,13 @@ public final class NetworkProtectionIconPublisher { // MARK: - Subscribing to NetP updates private func subscribeToConnectionStatusChanges() { - statusChangeCancellable = statusReporter.statusObserver.publisher.sink { [weak self] _ in + statusChangeCancellable = statusReporter.statusObserver.publisher.receive(on: RunLoop.main).sink { [weak self] _ in self?.updateMenuIcon() } } private func subscribeToConnectionIssues() { - connectivityIssuesCancellable = statusReporter.connectivityIssuesObserver.publisher.sink { [weak self] _ in + connectivityIssuesCancellable = statusReporter.connectivityIssuesObserver.publisher.receive(on: RunLoop.main).sink { [weak self] _ in self?.updateMenuIcon() } } From 7a2b37e4d7c623922356016d493b2dd90e5339a7 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 8 Apr 2024 13:29:53 -0700 Subject: [PATCH 029/221] Automatically close VPN popover when the app goes into the background (#2562) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206164244676889/f Tech Design URL: CC: Description: This PR updates the VPN popover to dismiss any time the app enters the background, in reaction to users occasionally being confused about why the UI appeared greyed out. We only do this if the user is already onboarded, as otherwise the popover keeps dismissing any time they go to complete some onboarding step. --- .../NetworkProtectionPopover.swift | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index 0acbf84065..073db45255 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -48,7 +48,7 @@ public final class NetworkProtectionPopover: NSPopover { private let debugInformationPublisher = CurrentValueSubject(false) private let statusReporter: NetworkProtectionStatusReporter private let model: NetworkProtectionStatusView.Model - private var appLifecycleCancellable: AnyCancellable? + private var appLifecycleCancellables = Set() public required init(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, @@ -101,11 +101,22 @@ public final class NetworkProtectionPopover: NSPopover { // MARK: - Status Refresh private func subscribeToAppLifecycleEvents() { - appLifecycleCancellable = NotificationCenter + NotificationCenter .default .publisher(for: NSApplication.didBecomeActiveNotification) - .sink { [weak self] _ in - self?.model.refreshLoginItemStatus() + .sink { [weak self] _ in self?.model.refreshLoginItemStatus() } + .store(in: &appLifecycleCancellables) + + NotificationCenter + .default + .publisher(for: NSApplication.didResignActiveNotification) + .sink { [weak self] _ in self?.closePopoverIfOnboarded() } + .store(in: &appLifecycleCancellables) + } + + private func closePopoverIfOnboarded() { + if self.model.onboardingStatus == .completed { + self.close() } } From 6483649b87288bf87152bad071f4658e1c4faea5 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 8 Apr 2024 14:40:30 -0700 Subject: [PATCH 030/221] Avoid caching a PrivacyConfig instance in FeatureFlagger (#2561) Task/Issue URL: https://app.asana.com/0/414235014887631/1206053496104935/f Tech Design URL: CC: Description: This PR updates FeatureFlagger to no longer cache a PrivacyConfig. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- DuckDuckGo/Application/AppDelegate.swift | 6 ++++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8391b71c55..f32dde62b9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14434,7 +14434,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 132.0.2; + version = 133.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ad6675f775..46c657c96d 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "5199a6964e183c3d001b188286bbabeca93c8849", - "version" : "132.0.2" + "revision" : "c0b0cb55e7ac2f69d10452e1a5c06713155d798e", + "version" : "133.0.0" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index c0307baaa8..22cc5ea79e 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -179,8 +179,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { AppPrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider), database: Database.shared) #endif - featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, - privacyConfig: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig) + featureFlagger = DefaultFeatureFlagger( + internalUserDecider: internalUserDecider, + privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager + ) #if SUBSCRIPTION #if APPSTORE || !STRIPE diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index e4c234a04f..46c094664b 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", exact: "132.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index ad0b87534e..d0ea0235c3 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "132.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index e055893291..556d4f308e 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: "132.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From d0fb45d8ac54852992b7281446affdc4b89a2c7f Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Tue, 9 Apr 2024 09:26:35 +1000 Subject: [PATCH 031/221] Fix bookmarks bar visibility (#2554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1207002191511983/f **Description**: The current bookmarks bar has some bugs related to its visibility when the appearance preference is “Only shows on New Tab”. This PR extracts and fix the logic to determine whether the bookmarks bar should be visible or not. --- DuckDuckGo.xcodeproj/project.pbxproj | 14 + .../BookmarksBarVisibilityManager.swift | 96 ++++++ .../MainWindow/MainViewController.swift | 28 +- .../Model/AppearancePreferences.swift | 2 + .../BookmarksBarVisibilityManagerTests.swift | 296 ++++++++++++++++++ 5 files changed, 418 insertions(+), 18 deletions(-) create mode 100644 DuckDuckGo/BookmarksBar/BookmarksBarVisibilityManager.swift create mode 100644 UnitTests/BookmarksBar/BookmarksBarVisibilityManagerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f32dde62b9..2bbc99a10b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2479,6 +2479,11 @@ 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; + 9F33445E2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F33445D2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift */; }; + 9F33445F2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F33445D2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift */; }; + 9F3344602BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F33445D2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift */; }; + 9F3344622BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3344612BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift */; }; + 9F3344632BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3344612BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift */; }; 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; @@ -4227,6 +4232,8 @@ 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; + 9F33445D2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarVisibilityManager.swift; sourceTree = ""; }; + 9F3344612BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarVisibilityManagerTests.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; @@ -5761,6 +5768,7 @@ isa = PBXGroup; children = ( 4B43468E285ED6CB00177407 /* ViewModel */, + 9F3344612BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift */, ); path = BookmarksBar; sourceTree = ""; @@ -6481,6 +6489,7 @@ children = ( 4BD18F02283F0F1000058124 /* View */, 850E8DFA2A6FEC5E00691187 /* BookmarksBarAppearance.swift */, + 9F33445D2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift */, ); path = BookmarksBar; sourceTree = ""; @@ -10528,6 +10537,7 @@ 4B9DB0422A983B24000927DB /* WaitlistDialogView.swift in Sources */, 3706FB57293F65D500E42796 /* AppPrivacyConfigurationDataProvider.swift in Sources */, 857E5AF62A790B7000FC0FB4 /* PixelExperiment.swift in Sources */, + 9F33445F2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, 3706FB58293F65D500E42796 /* LinkButton.swift in Sources */, 4B0EF7272B578096009D6481 /* AppVersionExtension.swift in Sources */, 3706FB59293F65D500E42796 /* TemporaryFileHandler.swift in Sources */, @@ -11231,6 +11241,7 @@ 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */, 3706FE6F293F661700E42796 /* LocalStatisticsStoreTests.swift in Sources */, 3706FE70293F661700E42796 /* HistoryCoordinatorTests.swift in Sources */, + 9F3344632BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, 3706FE71293F661700E42796 /* SavedStateMock.swift in Sources */, 3706FE72293F661700E42796 /* ClickToLoadTDSTests.swift in Sources */, 3706FE73293F661700E42796 /* PermissionManagerMock.swift in Sources */, @@ -11879,6 +11890,7 @@ 4B957A932AC7AE700062CA31 /* PermissionModel.swift in Sources */, 4B957A942AC7AE700062CA31 /* PasteboardFolder.swift in Sources */, 4B957A952AC7AE700062CA31 /* CookieManagedNotificationView.swift in Sources */, + 9F3344602BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, 4B957A962AC7AE700062CA31 /* PermissionType.swift in Sources */, 4B957A982AC7AE700062CA31 /* RecentlyClosedWindow.swift in Sources */, 4B957A992AC7AE700062CA31 /* ActionSpeech.swift in Sources */, @@ -12958,6 +12970,7 @@ AA7EB6DF27E7C57D00036718 /* MouseOverAnimationButton.swift in Sources */, AA7412B724D1687000D22FE0 /* TabBarScrollView.swift in Sources */, 1E559BB12BBCA9F1002B4AF6 /* RedirectNavigationResponder.swift in Sources */, + 9F33445E2BBFA77F0040CBEB /* BookmarksBarVisibilityManager.swift in Sources */, 4B9292D92667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift in Sources */, 14D9B8FB24F7E089000D4D13 /* AddressBarViewController.swift in Sources */, B65536A62685B82B00085A79 /* Permissions.swift in Sources */, @@ -13136,6 +13149,7 @@ AAC9C01C24CB594C00AD1325 /* TabViewModelTests.swift in Sources */, 567DA93F29E8045D008AC5EE /* MockEmailStorage.swift in Sources */, 37CD54B727F1B28A00F1F7B9 /* DefaultBrowserPreferencesTests.swift in Sources */, + 9F3344622BBFBDA40040CBEB /* BookmarksBarVisibilityManagerTests.swift in Sources */, 028904202A7B25380028369C /* AppConfigurationURLProviderTests.swift in Sources */, B65349AA265CF45000DCC645 /* DispatchQueueExtensionsTests.swift in Sources */, 858A798A26A9B35E00A75A42 /* PasswordManagementItemModelTests.swift in Sources */, diff --git a/DuckDuckGo/BookmarksBar/BookmarksBarVisibilityManager.swift b/DuckDuckGo/BookmarksBar/BookmarksBarVisibilityManager.swift new file mode 100644 index 0000000000..9e837b1a47 --- /dev/null +++ b/DuckDuckGo/BookmarksBar/BookmarksBarVisibilityManager.swift @@ -0,0 +1,96 @@ +// +// BookmarksBarVisibilityManager.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 Combine + +/// Decides if the BookmarksBar should be visible based on the Tab.Content and Appearance preferences. +final class BookmarksBarVisibilityManager { + private var bookmarkBarVisibilityCancellable: AnyCancellable? + private var bookmarkContentCancellable: AnyCancellable? + + /// A published value indicating the visibility of the bookmarks bar. + /// Returns`true` if the bookmarks bar is visible; otherwise, `false`. + @Published var isBookmarksBarVisible: Bool = false + + private let selectedTabPublisher: AnyPublisher + private let preferences: AppearancePreferences + + /// Create an instance given the specified `TabViewModel` publisher and `AppearancePreferences`. + /// - Parameters: + /// - selectedTabPublisher: A publisher that returns the selected Tab view model. + /// - preferences: The `AppearancePreferences` to read the bookmarks appearance preferences from. + init(selectedTabPublisher: AnyPublisher, preferences: AppearancePreferences = .shared) { + self.selectedTabPublisher = selectedTabPublisher + self.preferences = preferences + bind() + } + +} + +// MARK: - Private + +private extension BookmarksBarVisibilityManager { + + func bind() { + let bookmarksBarVisibilityPublisher = NotificationCenter.default + .publisher(for: AppearancePreferences.Notifications.showBookmarksBarSettingChanged) + + let bookmarksBarAppearancePublisher = NotificationCenter.default + .publisher(for: AppearancePreferences.Notifications.bookmarksBarSettingAppearanceChanged) + + let bookmarksBarNotificationsPublisher = Publishers.Merge(bookmarksBarVisibilityPublisher, bookmarksBarAppearancePublisher) + .map { _ in () } // Map To Void, we're not interested in the notification itself + .prepend(()) // Start with a value so combineLatest can fire + + // Every time the user select a tab or the Appeareance preference changes check if bookmarks bar should be visible or not. + // For the selected Tab we should also check if the Tab content changes as it can switch from empty to url if the user loads a web page. + bookmarkBarVisibilityCancellable = bookmarksBarNotificationsPublisher + .combineLatest(selectedTabPublisher) + .compactMap { _, selectedTab -> TabViewModel? in + guard let selectedTab else { return nil } + return selectedTab + } + .flatMap { tabViewModel in + // Subscribe to the selected tab content. + // When a tab is empty and the bookmarksBar should show only on empty Tabs it should disappear when we load a website. + tabViewModel.tab.$content.eraseToAnyPublisher() + } + .sink(receiveValue: { [weak self] tabContent in + guard let self = self else { return } + self.updateBookmarksBar(content: tabContent, preferences: self.preferences) + }) + } + + func updateBookmarksBar(content: Tab.TabContent, preferences: AppearancePreferences) { + // If visibility should be off, set visibility off and exit + guard preferences.showBookmarksBar else { + isBookmarksBarVisible = false + return + } + + // If visibility is on check Appearance + switch preferences.bookmarksBarAppearance { + case .newTabOnly: + isBookmarksBarVisible = content.isEmpty + case .alwaysOn: + isBookmarksBarVisible = true + } + } + +} diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 5e0840ebc5..04b5810087 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -32,6 +32,7 @@ final class MainViewController: NSViewController { let findInPageViewController: FindInPageViewController let fireViewController: FireViewController let bookmarksBarViewController: BookmarksBarViewController + private let bookmarksBarVisibilityManager: BookmarksBarVisibilityManager let tabCollectionViewModel: TabCollectionViewModel let isBurner: Bool @@ -60,6 +61,7 @@ final class MainViewController: NSViewController { self.isBurner = tabCollectionViewModel.isBurner tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) + bookmarksBarVisibilityManager = BookmarksBarVisibilityManager(selectedTabPublisher: tabCollectionViewModel.$selectedTabViewModel.eraseToAnyPublisher()) let networkProtectionPopoverManager: NetPPopoverManager = { #if DEBUG @@ -124,7 +126,7 @@ final class MainViewController: NSViewController { listenToKeyDownEvents() subscribeToMouseTrackingArea() subscribeToSelectedTabViewModel() - subscribeToAppSettingsNotifications() + subscribeToBookmarkBarVisibility() subscribeToFirstResponder() mainView.findInPageContainerView.applyDropShadow() @@ -171,9 +173,6 @@ final class MainViewController: NSViewController { resizeNavigationBar(isHomePage: tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab, animated: false) - - let bookmarksBarVisible = AppearancePreferences.shared.showBookmarksBar - updateBookmarksBarViewVisibility(visible: bookmarksBarVisible) } updateDividerColor(isShowingHomePage: tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab) @@ -314,11 +313,13 @@ final class MainViewController: NSViewController { .store(in: &tabViewModelCancellables) } - private func subscribeToAppSettingsNotifications() { - bookmarksBarVisibilityChangedCancellable = NotificationCenter.default - .publisher(for: AppearancePreferences.Notifications.showBookmarksBarSettingChanged) - .sink { [weak self] _ in - self?.updateBookmarksBarViewVisibility(visible: AppearancePreferences.shared.showBookmarksBar) + private func subscribeToBookmarkBarVisibility() { + bookmarksBarVisibilityChangedCancellable = bookmarksBarVisibilityManager + .$isBookmarksBarVisible + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] isBookmarksBarVisible in + self?.updateBookmarksBarViewVisibility(visible: isBookmarksBarVisible) } } @@ -335,7 +336,6 @@ final class MainViewController: NSViewController { defer { lastTabContent = content } resizeNavigationBar(isHomePage: content == .newtab, animated: content == .newtab && lastTabContent != .newtab) - updateBookmarksBar(content) adjustFirstResponder(selectedTabViewModel: selectedTabViewModel, tabContent: content) } .store(in: &self.tabViewModelCancellables) @@ -355,14 +355,6 @@ final class MainViewController: NSViewController { } } - private func updateBookmarksBar(_ content: Tab.TabContent, _ prefs: AppearancePreferences = AppearancePreferences.shared) { - if content.isUrl && prefs.bookmarksBarAppearance == .newTabOnly { - updateBookmarksBarViewVisibility(visible: false) - } else if prefs.showBookmarksBar { - updateBookmarksBarViewVisibility(visible: true) - } - } - private func subscribeToFindInPage(of selectedTabViewModel: TabViewModel?) { selectedTabViewModel?.findInPage? .$isVisible diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index 833bab73f0..c5558b2637 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -135,6 +135,7 @@ final class AppearancePreferences: ObservableObject { struct Notifications { static let showBookmarksBarSettingChanged = NSNotification.Name("ShowBookmarksBarSettingChanged") + static let bookmarksBarSettingAppearanceChanged = NSNotification.Name("BookmarksBarSettingAppearanceChanged") } static let shared = AppearancePreferences() @@ -197,6 +198,7 @@ final class AppearancePreferences: ObservableObject { @Published var bookmarksBarAppearance: BookmarksBarAppearance { didSet { persistor.bookmarksBarAppearance = bookmarksBarAppearance + NotificationCenter.default.post(name: Notifications.bookmarksBarSettingAppearanceChanged, object: nil) } } diff --git a/UnitTests/BookmarksBar/BookmarksBarVisibilityManagerTests.swift b/UnitTests/BookmarksBar/BookmarksBarVisibilityManagerTests.swift new file mode 100644 index 0000000000..802a33323c --- /dev/null +++ b/UnitTests/BookmarksBar/BookmarksBarVisibilityManagerTests.swift @@ -0,0 +1,296 @@ +// +// BookmarksBarVisibilityManagerTests.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 XCTest +import Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class BookmarksBarVisibilityManagerTests: XCTestCase { + private let selectedTabSubject = PassthroughSubject() + private var appearance: AppearancePreferences! + private var cancellables: Set! + let tabContents: [Tab.TabContent] = [ + .none, + .newtab, + .url(URL.duckDuckGo, credential: nil, source: .link), + .settings(pane: nil), + .bookmarks, + .onboarding, + .dataBrokerProtection, + .subscription(URL.duckDuckGo), + .identityTheftRestoration(URL.duckDuckGo) + ] + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + appearance = AppearancePreferences(persistor: AppearancePreferencesPersistorMock()) + } + + override func tearDownWithError() throws { + cancellables = nil + appearance = nil + try super.tearDownWithError() + } + + func makeSUT() -> BookmarksBarVisibilityManager { + return BookmarksBarVisibilityManager( + selectedTabPublisher: selectedTabSubject.eraseToAnyPublisher(), + preferences: appearance + ) + } + + func testWhenSubscribeThenIsBookmarksBarVisibleIsFalse() { + // GIVEN + let sut = makeSUT() + + // WHEN + let result = sut.isBookmarksBarVisible + + // THEN + XCTAssertFalse(result) + } + + // MARK: - Selecting a Tab + + // Appearance `showBookmarksBars` false + func testWhenSelectedTaContentAndShowBookmarksBarIsFalseThenIsBookmarksBarVisibleIsFalse() throws { + // GIVEN + appearance.showBookmarksBar = false + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + + // WHEN + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // THEN + try assertFalse(capturedValue) + } + } + + // Appearance `showBookmarksBars` true and .newTabOnly + + func testWhenShowBookmarksBarIsTrueAndBookmarkBarAppearanceIsTabOnlyThenIsBookmarksBarVisibleIsTrueForNoneAndNewTab() throws { + // GIVEN + appearance.showBookmarksBar = true + appearance.bookmarksBarAppearance = .newTabOnly + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + + // WHEN + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // THEN + switch content { + case .none, .newtab: + try assertTrue(capturedValue) + default: + try assertFalse(capturedValue) + } + } + } + + // Appearance `showBookmarksBars` true and .alwaysOn + + func testWhenShowBookmarksBarIsTrueAndBookmarkBarAppearanceIsAlwaysOnThenIsBookmarksBarVisibleIsTrue() throws { + // GIVEN + appearance.showBookmarksBar = true + appearance.bookmarksBarAppearance = .alwaysOn + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + + // WHEN + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // THEN + try assertTrue(capturedValue) + } + } + + // MARK: - Settings Change + + func testWhenChangingShowBookmarksBarToTrueThenIsBookmarksBarVisibleIsTrue() throws { + // GIVEN + appearance.showBookmarksBar = false + appearance.bookmarksBarAppearance = .alwaysOn + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + XCTAssertFalse(sut.isBookmarksBarVisible) + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // WHEN + appearance.showBookmarksBar = true + + // THEN + try assertTrue(capturedValue) + } + } + + func testWhenChangingShowBookmarksBarToFalseThenIsBookmarksBarVisibleIsFalse() throws { + // GIVEN + appearance.showBookmarksBar = true + appearance.bookmarksBarAppearance = .alwaysOn + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // WHEN + appearance.showBookmarksBar = false + + // THEN + try assertFalse(capturedValue) + } + } + + func testWhenBookmarksBarAppearanceChangesToAlwaysVisibleThenIsBookmarkBarVisibleIsTrue() throws { + // GIVEN + appearance.showBookmarksBar = true + appearance.bookmarksBarAppearance = .newTabOnly + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + XCTAssertFalse(sut.isBookmarksBarVisible) + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // WHEN + appearance.bookmarksBarAppearance = .alwaysOn + + // THEN + try assertTrue(capturedValue) + } + } + + func testWhenBookmarksBarAppearanceChangesToOnlyOnNewTabThenIsBookmarkBarVisibleIsTrueForNoneAndNewTab() throws { + // GIVEN + appearance.showBookmarksBar = true + appearance.bookmarksBarAppearance = .alwaysOn + + for content in tabContents { + var capturedValue: Bool? + let sut = makeSUT() + XCTAssertFalse(sut.isBookmarksBarVisible) + sut.$isBookmarksBarVisible + .dropFirst() // Not interested in the value when subscribing + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + selectedTabSubject.send(TabViewModel(tab: Tab(content: content))) + + // WHEN + appearance.bookmarksBarAppearance = .newTabOnly + + // THEN + switch content { + case .none, .newtab: + try assertTrue(capturedValue) + default: + try assertFalse(capturedValue) + } + } + } + + // MARK: - New Tab becoming URL + + func testWhenBookmarksBarAppeareanceIsNewTabOnlyAndTabContentBecomesURLThenIsBookmarkBarVisibleIsFalse() throws { + // GIVEN + appearance.showBookmarksBar = true + appearance.bookmarksBarAppearance = .newTabOnly + let sut = makeSUT() + let tab = Tab(content: .newtab) + selectedTabSubject.send(TabViewModel(tab: tab)) + + var capturedValue: Bool? + sut.$isBookmarksBarVisible + .sink { value in + capturedValue = value + } + .store(in: &cancellables) + try assertTrue(capturedValue) + + // WHEN + tab.setContent(.url(URL.duckDuckGo, credential: nil, source: .link)) + + // THEN + try assertFalse(capturedValue) + } + +} + +private extension BookmarksBarVisibilityManagerTests { + + func assertFalse(_ value: Bool?) throws { + let value = try XCTUnwrap(value) + XCTAssertFalse(value) + } + + func assertTrue(_ value: Bool?) throws { + let value = try XCTUnwrap(value) + XCTAssertTrue(value) + } + +} From 2985eb83e722a129bd249cd66359c229e95c7546 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 9 Apr 2024 02:19:38 +0000 Subject: [PATCH 032/221] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- .../AppTrackerDataSetProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 102 +- DuckDuckGo/ContentBlocker/trackerData.json | 2172 +++++++++++------ 4 files changed, 1441 insertions(+), 841 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index 07639c0bb4..f0052639b2 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"8838582254dfa215a99ea4d38e04cd20\"" - public static let embeddedDataSHA = "4851da5558dde2ec548d4e6ca4778e2040ad97c7edecf701de0e8ec907cb42bb" + public static let embeddedDataETag = "\"fd95ad4da437370f57ea8c2e2d03f48f\"" + public static let embeddedDataSHA = "f11d34eb516a2ba722c22e15ff8cdee5e5b2570adbf9d1b22d50438b30f57188" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift index 12d6d8fbbd..a182f36fd5 100644 --- a/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppTrackerDataSetProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"07bd7f610e3fa234856abcc2b56ab10e\"" - public static let embeddedDataSHA = "1d7ef8f4c5a717a5d82f43383e33290021358d6255db12b6fdd0928e28d123ee" + public static let embeddedDataETag = "\"ef8ebcc98d8abccca793c7e04422b160\"" + public static let embeddedDataSHA = "e2e8e5e191df54227222fbb0545a7eb8634b1156a69182323981bb6aed2c639d" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 4ddf3a01ba..216cd4c618 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1712071688011, + "version": 1712611145027, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -302,11 +302,12 @@ "disabledCMPs": [ "generic-cosmetic", "termsfeed3", - "strato.de" + "strato.de", + "healthline-media" ] }, "state": "enabled", - "hash": "98e57a3eb872c9dfb4a019b90dc6c0ec" + "hash": "44af0b568856ce87b825bb7fc61b6961" }, "autofill": { "exceptions": [ @@ -1096,7 +1097,7 @@ "reason": "https://github.com/duckduckgo/privacy-configuration/issues/667" }, { - "domain": "www.canva.com", + "domain": "canva.com", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1818" }, { @@ -1136,12 +1137,16 @@ { "domain": "sas.dk", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1347" + }, + { + "domain": "nationalmssociety.org", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/1963" } ] }, "exceptions": [], "state": "enabled", - "hash": "7edfa8344fd2577b31426130696d8b23" + "hash": "76976e1ac417949aae8cb1c7c7ca0a60" }, "dbp": { "state": "enabled", @@ -1184,7 +1189,9 @@ "videoElement": "#player video", "videoElementContainer": "#player .html5-video-player", "hoverExcluded": [], - "clickExcluded": [], + "clickExcluded": [ + "ytd-thumbnail-overlay-toggle-button-renderer" + ], "allowedEventTargets": [ ".ytp-inline-preview-scrim", ".ytd-video-preview", @@ -1231,7 +1238,7 @@ ] }, "state": "enabled", - "hash": "ab2c73639ad75b2d6efb18f324230397" + "hash": "b685397a8317384bec3b8d7e8b7571bb" }, "elementHiding": { "exceptions": [ @@ -1915,6 +1922,14 @@ { "selector": ".proper-dynamic-insertion", "type": "closest-empty" + }, + { + "selector": ".Page-header-leaderboardAd", + "type": "hide-empty" + }, + { + "selector": ".SovrnAd", + "type": "hide-empty" } ] }, @@ -2365,6 +2380,23 @@ } ] }, + { + "domain": "eurogamer.net", + "rules": [ + { + "selector": "#sticky_leaderboard", + "type": "hide-empty" + }, + { + "selector": ".primis_wrapper", + "type": "hide" + }, + { + "selector": ".autoad", + "type": "hide-empty" + } + ] + }, { "domain": "examiner.com.au", "rules": [ @@ -4222,7 +4254,7 @@ ] }, "state": "enabled", - "hash": "10545dae34a5b5f2cc26c91976be5809" + "hash": "ea31ebf0dd3e4831467ed2b2ec783279" }, "exceptionHandler": { "exceptions": [ @@ -4952,14 +4984,14 @@ "rollout": { "steps": [ { - "percent": 5 + "percent": 10 } ] } } }, "state": "enabled", - "hash": "f7cce63c16c142db4ff5764b542a6c52" + "hash": "b337f9c7cf15e7e4807ef232befaa999" }, "privacyPro": { "state": "enabled", @@ -5082,6 +5114,16 @@ "state": "disabled", "hash": "5e792dd491428702bc0104240fbce0ce" }, + "sslCertificates": { + "state": "enabled", + "exceptions": [], + "features": { + "allowBypass": { + "state": "enabled" + } + }, + "hash": "abe9584048f7f8157f71a14e7914cb1c" + }, "sync": { "state": "enabled", "features": { @@ -5366,6 +5408,7 @@ "fattoincasadabenedetta.it", "inquirer.com", "thesurfersview.com", + "twitchy.com", "wildrivers.lostcoastoutpost.com" ] }, @@ -5989,6 +6032,12 @@ "sbs.com.au" ] }, + { + "rule": "www3.doubleclick.net", + "domains": [ + "scrolller.com" + ] + }, { "rule": "doubleclick.net", "domains": [ @@ -6421,6 +6470,12 @@ "domains": [ "" ] + }, + { + "rule": "marketingplatform.google.com/about/enterprise", + "domains": [ + "scrolller.com" + ] } ] }, @@ -6429,19 +6484,7 @@ { "rule": "imasdk.googleapis.com/js/sdkloader/ima3.js", "domains": [ - "arkadium.com", - "bloomberg.com", - "cbssports.com", - "crunchyroll.com", - "gamak.tv", - "games.washingtonpost.com", - "metro.co.uk", - "nfl.com", - "pandora.com", - "paper-io.com", - "rawstory.com", - "usatoday.com", - "washingtonpost.com" + "" ] } ] @@ -6466,6 +6509,7 @@ "daotranslate.com", "drakescans.com", "duden.de", + "edealinfo.com", "freetubetv.net", "hscprojects.com", "kits4beats.com", @@ -7777,6 +7821,16 @@ } ] }, + "sundaysky.com": { + "rules": [ + { + "rule": "sundaysky.com", + "domains": [ + "bankofamerica.com" + ] + } + ] + }, "taboola.com": { "rules": [ { @@ -8209,7 +8263,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "adf596be88e8975a3cdcaaef3d24990d" + "hash": "936913b03c62ec1861b64a7a2316ddfd" }, "trackingCookies1p": { "settings": { diff --git a/DuckDuckGo/ContentBlocker/trackerData.json b/DuckDuckGo/ContentBlocker/trackerData.json index 647e15dbb6..f7618c909f 100644 --- a/DuckDuckGo/ContentBlocker/trackerData.json +++ b/DuckDuckGo/ContentBlocker/trackerData.json @@ -1,7 +1,7 @@ { "_builtWith": { - "tracker-radar": "09133e827d9dcbba9465c87efdf0229ddd910d3e867f8ccd5efc31abd7073963-4013b4e91930c643394cb31c6c745356f133b04f", - "tracker-surrogates": "ba0d8cefe4432723ec75b998241efd2454dff35a" + "tracker-radar": "74dd9601901673a7c0f87e609695b5a0e31b808adabd62e6db6ed7c99bde966d-4013b4e91930c643394cb31c6c745356f133b04f", + "tracker-surrogates": "0528e3226df15b1a3e319ad68ef76612a8f26623" }, "readme": "https://github.com/duckduckgo/tracker-blocklists", "trackers": { @@ -464,7 +464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -475,7 +475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -521,7 +521,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2409,7 +2409,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2420,7 +2420,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2464,7 +2464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2475,7 +2475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2590,7 +2590,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2724,7 +2724,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -2735,7 +2735,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3054,7 +3054,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3181,7 +3181,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3305,7 +3305,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3689,7 +3689,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3700,7 +3700,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3711,7 +3711,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3722,7 +3722,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3788,7 +3788,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3840,7 +3840,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3851,7 +3851,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -3988,7 +3988,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4339,7 +4339,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4374,7 +4374,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4820,7 +4820,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -4831,7 +4831,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5033,7 +5033,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5075,7 +5075,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5098,7 +5098,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5133,7 +5133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5162,7 +5162,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5450,7 +5450,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5567,7 +5567,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5578,7 +5578,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5589,7 +5589,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5600,7 +5600,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5637,7 +5637,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -5692,7 +5692,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6228,7 +6228,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6382,7 +6382,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6698,7 +6698,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6709,7 +6709,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -6829,7 +6829,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7025,7 +7025,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7061,7 +7061,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7072,7 +7072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7083,7 +7083,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7666,7 +7666,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7677,7 +7677,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7688,7 +7688,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7769,7 +7769,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7892,7 +7892,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7903,7 +7903,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7981,7 +7981,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -7992,7 +7992,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8107,7 +8107,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8258,7 +8258,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8269,7 +8269,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8760,7 +8760,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8771,7 +8771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -8812,7 +8812,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9228,7 +9228,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9833,7 +9833,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9844,7 +9844,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9855,7 +9855,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9895,7 +9895,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9937,7 +9937,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -9991,7 +9991,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10059,7 +10059,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10070,7 +10070,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10118,7 +10118,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10244,7 +10244,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10404,7 +10404,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10415,7 +10415,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10426,7 +10426,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10453,7 +10453,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10503,7 +10503,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -10550,7 +10550,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11028,7 +11028,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11068,7 +11068,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11804,7 +11804,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11893,7 +11893,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -11904,7 +11904,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12126,7 +12126,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12166,7 +12166,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12177,7 +12177,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12188,7 +12188,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12199,7 +12199,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12525,7 +12525,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12536,7 +12536,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -12547,7 +12547,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -14455,7 +14455,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15140,7 +15140,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15215,7 +15215,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -15266,7 +15266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16125,7 +16125,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16136,7 +16136,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16165,7 +16165,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16384,7 +16384,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16693,7 +16693,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16704,7 +16704,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16889,7 +16889,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -16938,7 +16938,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17212,7 +17212,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17223,7 +17223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -17786,7 +17786,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18134,7 +18134,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18273,7 +18273,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18632,7 +18632,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -18843,7 +18843,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20020,7 +20020,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20241,7 +20241,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20252,7 +20252,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20263,7 +20263,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20403,7 +20403,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20890,7 +20890,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20919,7 +20919,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20930,7 +20930,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -20941,7 +20941,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21005,7 +21005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21085,7 +21085,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21133,7 +21133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21144,7 +21144,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21184,7 +21184,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21195,7 +21195,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21230,7 +21230,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21241,7 +21241,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21374,7 +21374,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21458,7 +21458,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21929,7 +21929,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21940,7 +21940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -21951,7 +21951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22021,7 +22021,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22032,7 +22032,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22185,7 +22185,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22196,7 +22196,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22238,7 +22238,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22249,7 +22249,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22260,7 +22260,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22530,7 +22530,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22541,7 +22541,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22602,7 +22602,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22660,7 +22660,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22671,7 +22671,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22771,7 +22771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22794,7 +22794,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -22805,7 +22805,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23162,7 +23162,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23266,7 +23266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23402,7 +23402,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23477,7 +23477,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23488,7 +23488,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23532,7 +23532,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23543,7 +23543,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23554,7 +23554,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23565,7 +23565,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23645,7 +23645,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23678,7 +23678,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23729,7 +23729,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23865,7 +23865,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -23994,7 +23994,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24005,7 +24005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24252,7 +24252,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24263,7 +24263,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24274,7 +24274,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24555,7 +24555,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24590,7 +24590,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24631,7 +24631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24747,7 +24747,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24758,7 +24758,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24787,7 +24787,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24883,7 +24883,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24940,7 +24940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -24951,7 +24951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25072,7 +25072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25229,7 +25229,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25255,7 +25255,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25266,7 +25266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25329,7 +25329,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25340,7 +25340,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25351,7 +25351,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25582,7 +25582,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25609,7 +25609,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25620,7 +25620,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25631,7 +25631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25661,7 +25661,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25672,7 +25672,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25700,7 +25700,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25711,7 +25711,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25784,7 +25784,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25832,7 +25832,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25843,7 +25843,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25854,7 +25854,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25865,7 +25865,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25876,7 +25876,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25887,7 +25887,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25959,7 +25959,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -25970,7 +25970,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26024,7 +26024,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26226,7 +26226,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26534,7 +26534,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26545,7 +26545,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26556,7 +26556,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26807,7 +26807,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -26818,7 +26818,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -27236,7 +27236,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28093,7 +28093,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28223,7 +28223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28234,7 +28234,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28367,7 +28367,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28450,7 +28450,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28461,7 +28461,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -28822,7 +28822,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29129,7 +29129,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29140,7 +29140,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29266,7 +29266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -29277,7 +29277,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -31019,7 +31019,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -31324,7 +31324,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32484,7 +32484,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32495,7 +32495,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32506,7 +32506,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32517,7 +32517,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32528,7 +32528,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32539,7 +32539,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32550,7 +32550,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "accurateanimal.com": { + "domain": "accurateanimal.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32561,7 +32572,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32572,7 +32583,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32583,7 +32594,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32594,7 +32605,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32605,7 +32616,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32616,7 +32627,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32627,7 +32638,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32638,7 +32649,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32649,7 +32660,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32660,7 +32671,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32671,7 +32682,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32682,7 +32693,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32693,7 +32704,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32704,7 +32715,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32715,7 +32726,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32726,7 +32737,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32737,7 +32748,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32748,7 +32759,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32759,7 +32770,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32770,7 +32781,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32781,7 +32792,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32792,7 +32803,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32803,7 +32814,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32814,7 +32825,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32825,7 +32836,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32836,7 +32847,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32847,7 +32858,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32858,7 +32869,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32869,7 +32880,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32880,7 +32891,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32891,7 +32902,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32902,7 +32913,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32913,7 +32924,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32924,7 +32935,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32935,7 +32946,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32946,7 +32957,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32957,7 +32968,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32968,7 +32979,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32979,7 +32990,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -32990,7 +33001,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33001,7 +33012,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33012,7 +33023,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33023,7 +33034,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33034,7 +33045,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33045,7 +33056,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33056,7 +33067,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33067,7 +33078,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33078,7 +33089,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33089,7 +33100,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33100,7 +33111,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33111,7 +33122,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33122,7 +33133,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33133,7 +33144,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33144,7 +33155,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33155,7 +33166,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33166,7 +33177,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33177,7 +33188,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33188,7 +33199,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33199,7 +33210,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33210,7 +33221,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33221,7 +33232,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33232,7 +33243,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33243,7 +33254,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33254,7 +33265,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33265,7 +33276,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33276,7 +33287,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33287,7 +33298,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33298,7 +33309,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33309,7 +33320,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33320,7 +33331,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33331,7 +33342,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "calypsocapsule.com": { + "domain": "calypsocapsule.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33342,7 +33364,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33353,7 +33375,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33364,7 +33386,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33375,7 +33397,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33386,7 +33408,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33397,7 +33419,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33408,7 +33430,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33419,7 +33441,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33430,7 +33452,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33441,7 +33463,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33452,7 +33474,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33463,7 +33485,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33474,7 +33496,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33485,7 +33507,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33496,7 +33518,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "chaireggnog.com": { + "domain": "chaireggnog.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "chairsdonkey.com": { + "domain": "chairsdonkey.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33507,7 +33551,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33518,7 +33562,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33529,7 +33573,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33540,7 +33584,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33551,7 +33595,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33562,7 +33606,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "chipperisle.com": { + "domain": "chipperisle.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "chivalrouscord.com": { + "domain": "chivalrouscord.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33573,7 +33639,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33584,7 +33650,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33595,7 +33661,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33606,7 +33672,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33617,7 +33683,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "cobaltoverture.com": { + "domain": "cobaltoverture.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33628,7 +33705,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33639,7 +33716,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33650,7 +33727,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33661,7 +33738,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33672,7 +33749,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33683,7 +33760,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33694,7 +33771,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33705,7 +33782,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33716,7 +33793,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33727,7 +33804,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33738,7 +33815,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33749,7 +33826,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33760,7 +33837,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33771,7 +33848,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33782,7 +33859,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33793,7 +33870,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33804,7 +33881,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33815,7 +33892,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33826,7 +33903,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33837,7 +33914,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33848,7 +33925,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33859,7 +33936,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33870,7 +33947,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "creatorpassenger.com": { + "domain": "creatorpassenger.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33881,7 +33969,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33892,7 +33980,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33903,7 +33991,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33914,7 +34002,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33925,7 +34013,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33936,7 +34024,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33947,7 +34035,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33958,7 +34046,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33969,7 +34057,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33980,7 +34068,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -33991,7 +34079,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34002,7 +34090,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34013,7 +34101,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34024,7 +34112,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34035,7 +34123,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34046,7 +34134,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34057,7 +34145,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34068,7 +34156,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34079,7 +34167,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34090,7 +34178,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34101,7 +34189,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34112,7 +34200,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34123,7 +34211,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34134,7 +34222,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34145,7 +34233,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34156,7 +34244,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34167,7 +34255,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34178,7 +34266,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34189,7 +34277,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34200,7 +34288,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34211,7 +34299,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34222,7 +34310,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34233,7 +34321,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34244,7 +34332,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34255,7 +34343,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34266,7 +34354,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "eagerknight.com": { + "domain": "eagerknight.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "echoinghaven.com": { + "domain": "echoinghaven.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34277,7 +34387,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34288,7 +34398,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "effulgenttempest.com": { + "domain": "effulgenttempest.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34299,7 +34420,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34310,7 +34431,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34321,7 +34442,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34332,7 +34453,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34343,7 +34464,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34354,7 +34475,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34365,7 +34486,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34376,7 +34497,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "engineertrick.com": { + "domain": "engineertrick.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34387,7 +34519,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34398,7 +34530,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34409,7 +34541,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34420,7 +34552,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34431,7 +34563,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34442,7 +34574,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34453,7 +34585,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34464,7 +34596,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34475,7 +34607,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34486,7 +34618,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34497,7 +34629,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34508,7 +34640,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "exquisiteartisanship.com": { + "domain": "exquisiteartisanship.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34519,7 +34662,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34530,7 +34673,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34541,7 +34684,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34552,7 +34695,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34563,7 +34706,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34574,7 +34717,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34585,7 +34728,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34596,7 +34739,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34607,7 +34750,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34618,7 +34761,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34629,7 +34772,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34640,7 +34783,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34651,7 +34794,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34662,7 +34805,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34673,7 +34816,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34684,7 +34827,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34695,7 +34838,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "flameuncle.com": { + "domain": "flameuncle.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34706,7 +34860,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34717,7 +34871,40 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "flourishingcollaboration.com": { + "domain": "flourishingcollaboration.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "flourishinginnovation.com": { + "domain": "flourishinginnovation.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "flourishingpartnership.com": { + "domain": "flourishingpartnership.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34728,7 +34915,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34739,7 +34926,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34750,7 +34937,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34761,7 +34948,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34772,7 +34959,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34783,7 +34970,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34794,7 +34981,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34805,7 +34992,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34816,7 +35003,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34827,7 +35014,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34838,7 +35025,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34849,7 +35036,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34860,7 +35047,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34871,7 +35058,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34882,7 +35069,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34893,7 +35080,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34904,7 +35091,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34915,7 +35102,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "gladysway.com": { + "domain": "gladysway.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34926,7 +35124,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34937,7 +35135,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "glitteringbrook.com": { + "domain": "glitteringbrook.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "goldfishgrowth.com": { + "domain": "goldfishgrowth.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34948,7 +35168,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34959,7 +35179,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34970,7 +35190,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34981,7 +35201,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -34992,7 +35212,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35003,7 +35223,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35014,7 +35234,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35025,7 +35245,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35036,7 +35256,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35047,7 +35267,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35058,7 +35278,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35069,7 +35289,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35080,7 +35300,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35091,7 +35311,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35102,7 +35322,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35113,7 +35333,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35124,7 +35344,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35135,7 +35355,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35146,7 +35366,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35157,7 +35377,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35168,7 +35388,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35179,7 +35399,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35190,7 +35410,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35201,7 +35421,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35212,7 +35432,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35223,7 +35443,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35234,7 +35454,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35245,7 +35465,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35256,7 +35476,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35267,7 +35487,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35278,7 +35498,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35289,7 +35509,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35300,7 +35520,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35311,7 +35531,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35322,7 +35542,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35333,7 +35553,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35344,7 +35564,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35355,7 +35575,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35366,7 +35586,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "impulselumber.com": { + "domain": "impulselumber.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35377,7 +35608,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35388,7 +35619,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35399,7 +35630,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35410,7 +35641,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35421,7 +35652,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35432,7 +35663,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35443,7 +35674,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35454,7 +35685,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35465,7 +35696,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35476,7 +35707,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35487,7 +35718,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35498,7 +35729,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35509,7 +35740,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "keenquill.com": { + "domain": "keenquill.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35520,7 +35762,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35531,7 +35773,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35542,7 +35784,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35553,7 +35795,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35564,7 +35806,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35575,7 +35817,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "lighttalon.com": { + "domain": "lighttalon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35586,7 +35839,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35597,7 +35850,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35608,7 +35861,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35619,7 +35872,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35630,7 +35883,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35641,7 +35894,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35652,7 +35905,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35663,7 +35916,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35674,7 +35927,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35685,7 +35938,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35696,7 +35949,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35707,7 +35960,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35718,7 +35971,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35729,7 +35982,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35740,7 +35993,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "majesticwaterscape.com": { + "domain": "majesticwaterscape.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35751,7 +36015,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35762,7 +36026,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35773,7 +36037,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35784,7 +36048,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35795,7 +36059,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35806,7 +36070,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35817,7 +36081,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "melodiouscomposition.com": { + "domain": "melodiouscomposition.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35828,7 +36103,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35839,7 +36114,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35850,7 +36125,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35861,7 +36136,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35872,7 +36147,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35883,7 +36158,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35894,7 +36169,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35905,7 +36180,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35916,7 +36191,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35927,7 +36202,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35938,7 +36213,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35949,7 +36224,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35960,7 +36235,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35971,7 +36246,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35982,7 +36257,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -35993,7 +36268,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36004,7 +36279,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36015,7 +36290,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36026,7 +36301,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36037,7 +36312,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36048,7 +36323,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36059,7 +36334,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36070,7 +36345,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36081,7 +36356,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36092,7 +36367,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36103,7 +36378,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36114,7 +36389,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36125,7 +36400,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36136,7 +36411,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36147,7 +36422,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36158,7 +36433,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36169,7 +36444,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36180,7 +36455,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36191,7 +36466,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36202,7 +36477,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36213,7 +36488,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36224,7 +36499,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36235,7 +36510,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "opulentsylvan.com": { + "domain": "opulentsylvan.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36246,7 +36532,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36257,7 +36543,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36268,7 +36554,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36279,7 +36565,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36290,7 +36576,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36301,7 +36587,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36312,7 +36598,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36323,7 +36609,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36334,7 +36620,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36345,7 +36631,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36356,7 +36642,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36367,7 +36653,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36378,7 +36664,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36389,7 +36675,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36400,7 +36686,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36411,7 +36697,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36422,7 +36708,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "pluckyzone.com": { + "domain": "pluckyzone.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36433,7 +36730,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36444,7 +36741,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36455,7 +36752,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36466,7 +36763,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "polishedfolly.com": { + "domain": "polishedfolly.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36477,7 +36785,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36488,7 +36796,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "popplantation.com": { + "domain": "popplantation.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36499,7 +36818,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36510,7 +36829,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36521,7 +36840,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36532,7 +36851,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36543,7 +36862,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36554,7 +36873,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "publicsofa.com": { + "domain": "publicsofa.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36565,7 +36895,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "pulsatingmeadow.com": { + "domain": "pulsatingmeadow.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36576,7 +36917,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36587,7 +36928,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36598,7 +36939,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36609,7 +36950,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36620,7 +36961,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36631,7 +36972,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36642,7 +36983,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36653,7 +36994,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36664,7 +37005,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36675,7 +37016,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36686,7 +37027,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36697,7 +37038,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36708,7 +37049,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36719,7 +37060,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36730,7 +37071,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36741,7 +37082,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36752,7 +37093,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36763,7 +37104,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36774,7 +37115,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36785,7 +37126,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36796,7 +37137,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36807,7 +37148,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36818,7 +37159,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "relationrest.com": { + "domain": "relationrest.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "rememberdiscussion.com": { + "domain": "rememberdiscussion.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36829,7 +37192,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36840,7 +37203,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36851,7 +37214,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36862,7 +37225,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36873,7 +37236,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36884,7 +37247,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36895,7 +37258,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36906,7 +37269,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36917,7 +37280,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36928,7 +37291,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36939,7 +37302,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36950,7 +37313,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36961,7 +37324,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36972,7 +37335,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36983,7 +37346,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -36994,7 +37357,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37005,7 +37368,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37016,7 +37379,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37027,7 +37390,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37038,7 +37401,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37049,7 +37412,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37060,7 +37423,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37071,7 +37434,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37082,7 +37445,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37093,7 +37456,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37104,7 +37467,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37115,7 +37478,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37126,7 +37489,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37137,7 +37500,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37148,7 +37511,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37159,7 +37522,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37170,7 +37533,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37181,7 +37544,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37192,7 +37555,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37203,7 +37566,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "serenecascade.com": { + "domain": "serenecascade.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37214,7 +37588,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37225,7 +37599,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37236,7 +37610,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37247,7 +37621,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37258,7 +37632,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37269,7 +37643,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37280,7 +37654,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37291,7 +37665,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37302,7 +37676,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37313,7 +37687,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37324,7 +37698,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37335,7 +37709,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37346,7 +37720,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37357,7 +37731,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37368,7 +37742,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37379,7 +37753,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37390,7 +37764,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37401,7 +37775,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37412,7 +37786,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37423,7 +37797,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37434,7 +37808,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37445,7 +37819,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37456,7 +37830,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37467,7 +37841,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37478,7 +37852,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37489,7 +37863,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37500,7 +37874,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37511,7 +37885,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37522,7 +37896,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37533,7 +37907,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37544,7 +37918,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37555,7 +37929,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37566,7 +37940,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37577,7 +37951,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37588,7 +37962,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37599,7 +37973,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37610,7 +37984,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37621,7 +37995,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37632,7 +38006,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37643,7 +38017,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37654,7 +38028,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37665,7 +38039,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37676,7 +38050,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37687,7 +38061,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37698,7 +38072,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37709,7 +38083,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37720,7 +38094,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37731,7 +38105,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37742,7 +38116,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37753,7 +38127,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37764,7 +38138,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37775,7 +38149,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37786,7 +38160,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37797,7 +38171,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37808,7 +38182,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37819,7 +38193,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37830,7 +38204,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37841,7 +38215,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37852,7 +38226,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37863,7 +38237,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37874,7 +38248,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37885,7 +38259,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37896,7 +38270,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37907,7 +38281,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37918,7 +38292,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37929,7 +38303,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37940,7 +38314,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37951,7 +38325,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37962,7 +38336,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37973,7 +38347,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "sublimequartz.com": { + "domain": "sublimequartz.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37984,7 +38369,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -37995,7 +38380,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38006,7 +38391,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38017,7 +38402,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38028,7 +38413,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38039,7 +38424,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38050,7 +38435,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38061,7 +38446,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38072,7 +38457,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38083,7 +38468,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38094,7 +38479,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38105,7 +38490,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38116,7 +38501,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38127,7 +38512,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38138,7 +38523,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38149,7 +38534,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "tearfulglass.com": { + "domain": "tearfulglass.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38160,7 +38556,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38171,7 +38567,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38182,7 +38578,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38193,7 +38589,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38204,7 +38600,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38215,7 +38611,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38226,7 +38622,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38237,7 +38633,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38248,7 +38644,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38259,7 +38655,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "thrivingmarketplace.com": { + "domain": "thrivingmarketplace.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38270,7 +38677,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38281,7 +38688,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38292,7 +38699,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38303,7 +38710,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38314,7 +38721,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38325,7 +38732,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38336,7 +38743,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38347,7 +38754,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38358,7 +38765,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38369,7 +38776,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38380,7 +38787,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38391,7 +38798,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38402,7 +38809,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38413,7 +38820,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38424,7 +38831,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38435,7 +38842,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38446,7 +38853,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38457,7 +38864,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38468,7 +38875,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38479,7 +38886,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38490,7 +38897,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38501,7 +38908,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38512,7 +38919,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38523,7 +38930,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38534,7 +38941,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38545,7 +38952,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38556,7 +38963,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38567,7 +38974,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38578,7 +38985,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38589,7 +38996,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38600,7 +39007,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38611,7 +39018,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38622,7 +39029,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38633,7 +39040,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38644,7 +39051,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38655,7 +39062,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38666,7 +39073,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38677,7 +39084,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38688,7 +39095,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38699,7 +39106,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vibrantcelebration.com": { + "domain": "vibrantcelebration.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38710,7 +39128,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38721,7 +39139,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38732,7 +39150,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38743,7 +39161,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38754,7 +39172,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38765,7 +39183,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "vividfrost.com": { + "domain": "vividfrost.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38776,7 +39205,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38787,7 +39216,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38798,7 +39227,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38809,7 +39238,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38820,7 +39249,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38831,7 +39260,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38842,7 +39271,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38853,7 +39282,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38864,7 +39293,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38875,7 +39304,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38886,7 +39315,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38897,7 +39326,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38908,7 +39337,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38919,7 +39348,18 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "wittyshack.com": { + "domain": "wittyshack.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38930,7 +39370,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38941,7 +39381,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38952,7 +39392,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38963,7 +39403,29 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "zestyhorizon.com": { + "domain": "zestyhorizon.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, + "fingerprinting": 1, + "cookies": 0.01, + "default": "block" + }, + "zestyrover.com": { + "domain": "zestyrover.com", + "owner": { + "name": "Leven Labs, Inc. DBA Admiral", + "displayName": "Admiral" + }, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38974,7 +39436,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -38985,7 +39447,7 @@ "name": "Leven Labs, Inc. DBA Admiral", "displayName": "Admiral" }, - "prevalence": 0.0151, + "prevalence": 0.0129, "fingerprinting": 1, "cookies": 0.01, "default": "block" @@ -48993,6 +49455,7 @@ "abstractedamount.com", "abstractedauthority.com", "acceptableauthority.com", + "accurateanimal.com", "accuratecoal.com", "acidpigs.com", "actoramusement.com", @@ -49088,6 +49551,7 @@ "calculatorstatement.com", "callousbrake.com", "calmcactus.com", + "calypsocapsule.com", "capablecup.com", "capriciouscorn.com", "captivatingcanyon.com", @@ -49107,6 +49571,8 @@ "ceciliavenus.com", "celestialquasar.com", "celestialspectra.com", + "chaireggnog.com", + "chairsdonkey.com", "chalkoil.com", "changeablecats.com", "chargecracker.com", @@ -49118,6 +49584,8 @@ "childlikeexample.com", "childlikeform.com", "chinsnakes.com", + "chipperisle.com", + "chivalrouscord.com", "chunkycactus.com", "circlelevel.com", "cleanhaircut.com", @@ -49125,6 +49593,7 @@ "cloisteredcurve.com", "closedcows.com", "coatfood.com", + "cobaltoverture.com", "coldbalance.com", "colossalclouds.com", "colossalcoat.com", @@ -49152,6 +49621,7 @@ "crabbychin.com", "cratecamera.com", "creatorcherry.com", + "creatorpassenger.com", "creaturecabbage.com", "crimsonmeadow.com", "critictruck.com", @@ -49204,8 +49674,11 @@ "drollwharf.com", "dustydime.com", "dustyhammer.com", + "eagerknight.com", + "echoinghaven.com", "effervescentcoral.com", "effervescentvista.com", + "effulgenttempest.com", "elasticchange.com", "elderlybean.com", "elusivebreeze.com", @@ -49215,6 +49688,7 @@ "encouragingthread.com", "endurablebulb.com", "energeticladybug.com", + "engineertrick.com", "enigmaticcanyon.com", "enigmaticvoyage.com", "enormousearth.com", @@ -49230,6 +49704,7 @@ "executeknowledge.com", "exhibitsneeze.com", "expansioneggnog.com", + "exquisiteartisanship.com", "exuberantedge.com", "fadedsnow.com", "fadewaves.com", @@ -49253,8 +49728,12 @@ "financefear.com", "firstfrogs.com", "fixedfold.com", + "flameuncle.com", "flimsycircle.com", "flimsythought.com", + "flourishingcollaboration.com", + "flourishinginnovation.com", + "flourishingpartnership.com", "flowerstreatment.com", "flowerycreature.com", "floweryfact.com", @@ -49284,9 +49763,12 @@ "giddycoat.com", "giraffepiano.com", "givevacation.com", + "gladysway.com", "gleamingcow.com", "glisteningguide.com", + "glitteringbrook.com", "gloriousbeef.com", + "goldfishgrowth.com", "gondolagnome.com", "gorgeousedge.com", "gracefulmilk.com", @@ -49337,6 +49819,7 @@ "importantmeat.com", "impossibleexpansion.com", "impulsejewel.com", + "impulselumber.com", "incompetentjoke.com", "inconclusiveaction.com", "inputicicle.com", @@ -49351,6 +49834,7 @@ "jubilanttempest.com", "jubilantwhisper.com", "kaputquill.com", + "keenquill.com", "knitstamp.com", "knottyswing.com", "laboredlocket.com", @@ -49360,6 +49844,7 @@ "leftliquid.com", "liftedknowledge.com", "lightenafterthought.com", + "lighttalon.com", "livelumber.com", "livelylaugh.com", "livelyreward.com", @@ -49379,6 +49864,7 @@ "lunchroomlock.com", "lustroushaven.com", "maddeningpowder.com", + "majesticwaterscape.com", "maliciousmusic.com", "marketspiders.com", "marriedbelief.com", @@ -49390,6 +49876,7 @@ "meatydime.com", "meddleplant.com", "melodiouschorus.com", + "melodiouscomposition.com", "meltmilk.com", "memopilot.com", "memorizematch.com", @@ -49435,6 +49922,7 @@ "oldfashionedoffer.com", "operationchicken.com", "optimallimit.com", + "opulentsylvan.com", "orientedargument.com", "outstandingincome.com", "outstandingsnails.com", @@ -49462,13 +49950,16 @@ "pleasantpump.com", "plotrabbit.com", "pluckypocket.com", + "pluckyzone.com", "pocketfaucet.com", "poeticpackage.com", "pointdigestion.com", "pointlesspocket.com", "pointlessprofit.com", + "polishedfolly.com", "politeplanes.com", "politicalporter.com", + "popplantation.com", "possibleboats.com", "possiblepencil.com", "potatoinvention.com", @@ -49484,7 +49975,9 @@ "profusesupport.com", "protestcopy.com", "psychedelicarithmetic.com", + "publicsofa.com", "puffypurpose.com", + "pulsatingmeadow.com", "pumpedpancake.com", "punyplant.com", "purposepipe.com", @@ -49519,6 +50012,8 @@ "regularplants.com", "regulatesleet.com", "rehabilitatereason.com", + "relationrest.com", + "rememberdiscussion.com", "repeatsweater.com", "replaceroute.com", "resonantbrush.com", @@ -49576,6 +50071,7 @@ "selfishsnake.com", "separatesort.com", "seraphicjubilee.com", + "serenecascade.com", "serenepebble.com", "serioussuit.com", "serpentshampoo.com", @@ -49679,6 +50175,7 @@ "stupendoussleet.com", "stupendoussnow.com", "stupidscene.com", + "sublimequartz.com", "succeedscene.com", "sugarfriction.com", "suggestionbridge.com", @@ -49700,6 +50197,7 @@ "tangycover.com", "tastelesstrees.com", "tastelesstrucks.com", + "tearfulglass.com", "tediousticket.com", "teenytinycellar.com", "teenytinyshirt.com", @@ -49715,6 +50213,7 @@ "thomastorch.com", "thoughtlessknot.com", "threetruck.com", + "thrivingmarketplace.com", "ticketaunt.com", "tidymitten.com", "tiredthroat.com", @@ -49763,12 +50262,14 @@ "verdantlabyrinth.com", "verdantloom.com", "verseballs.com", + "vibrantcelebration.com", "vibrantgale.com", "vibranthaven.com", "vibrantpact.com", "vibranttalisman.com", "virtualvincent.com", "vividcanopy.com", + "vividfrost.com", "vividmeadow.com", "vividplume.com", "volatileprofit.com", @@ -49787,15 +50288,18 @@ "whispermeeting.com", "wildcommittee.com", "wistfulwaste.com", + "wittyshack.com", "workoperation.com", "wretchedfloor.com", "wrongwound.com", "zephyrlabyrinth.com", "zestycrime.com", + "zestyhorizon.com", + "zestyrover.com", "zipperxray.com", "zlp6s.pw" ], - "prevalence": 0.0151, + "prevalence": 0.0129, "displayName": "Admiral" } }, @@ -50523,6 +51027,7 @@ "abstractedamount.com": "Leven Labs, Inc. DBA Admiral", "abstractedauthority.com": "Leven Labs, Inc. DBA Admiral", "acceptableauthority.com": "Leven Labs, Inc. DBA Admiral", + "accurateanimal.com": "Leven Labs, Inc. DBA Admiral", "accuratecoal.com": "Leven Labs, Inc. DBA Admiral", "acidpigs.com": "Leven Labs, Inc. DBA Admiral", "actoramusement.com": "Leven Labs, Inc. DBA Admiral", @@ -50618,6 +51123,7 @@ "calculatorstatement.com": "Leven Labs, Inc. DBA Admiral", "callousbrake.com": "Leven Labs, Inc. DBA Admiral", "calmcactus.com": "Leven Labs, Inc. DBA Admiral", + "calypsocapsule.com": "Leven Labs, Inc. DBA Admiral", "capablecup.com": "Leven Labs, Inc. DBA Admiral", "capriciouscorn.com": "Leven Labs, Inc. DBA Admiral", "captivatingcanyon.com": "Leven Labs, Inc. DBA Admiral", @@ -50637,6 +51143,8 @@ "ceciliavenus.com": "Leven Labs, Inc. DBA Admiral", "celestialquasar.com": "Leven Labs, Inc. DBA Admiral", "celestialspectra.com": "Leven Labs, Inc. DBA Admiral", + "chaireggnog.com": "Leven Labs, Inc. DBA Admiral", + "chairsdonkey.com": "Leven Labs, Inc. DBA Admiral", "chalkoil.com": "Leven Labs, Inc. DBA Admiral", "changeablecats.com": "Leven Labs, Inc. DBA Admiral", "chargecracker.com": "Leven Labs, Inc. DBA Admiral", @@ -50648,6 +51156,8 @@ "childlikeexample.com": "Leven Labs, Inc. DBA Admiral", "childlikeform.com": "Leven Labs, Inc. DBA Admiral", "chinsnakes.com": "Leven Labs, Inc. DBA Admiral", + "chipperisle.com": "Leven Labs, Inc. DBA Admiral", + "chivalrouscord.com": "Leven Labs, Inc. DBA Admiral", "chunkycactus.com": "Leven Labs, Inc. DBA Admiral", "circlelevel.com": "Leven Labs, Inc. DBA Admiral", "cleanhaircut.com": "Leven Labs, Inc. DBA Admiral", @@ -50655,6 +51165,7 @@ "cloisteredcurve.com": "Leven Labs, Inc. DBA Admiral", "closedcows.com": "Leven Labs, Inc. DBA Admiral", "coatfood.com": "Leven Labs, Inc. DBA Admiral", + "cobaltoverture.com": "Leven Labs, Inc. DBA Admiral", "coldbalance.com": "Leven Labs, Inc. DBA Admiral", "colossalclouds.com": "Leven Labs, Inc. DBA Admiral", "colossalcoat.com": "Leven Labs, Inc. DBA Admiral", @@ -50682,6 +51193,7 @@ "crabbychin.com": "Leven Labs, Inc. DBA Admiral", "cratecamera.com": "Leven Labs, Inc. DBA Admiral", "creatorcherry.com": "Leven Labs, Inc. DBA Admiral", + "creatorpassenger.com": "Leven Labs, Inc. DBA Admiral", "creaturecabbage.com": "Leven Labs, Inc. DBA Admiral", "crimsonmeadow.com": "Leven Labs, Inc. DBA Admiral", "critictruck.com": "Leven Labs, Inc. DBA Admiral", @@ -50734,8 +51246,11 @@ "drollwharf.com": "Leven Labs, Inc. DBA Admiral", "dustydime.com": "Leven Labs, Inc. DBA Admiral", "dustyhammer.com": "Leven Labs, Inc. DBA Admiral", + "eagerknight.com": "Leven Labs, Inc. DBA Admiral", + "echoinghaven.com": "Leven Labs, Inc. DBA Admiral", "effervescentcoral.com": "Leven Labs, Inc. DBA Admiral", "effervescentvista.com": "Leven Labs, Inc. DBA Admiral", + "effulgenttempest.com": "Leven Labs, Inc. DBA Admiral", "elasticchange.com": "Leven Labs, Inc. DBA Admiral", "elderlybean.com": "Leven Labs, Inc. DBA Admiral", "elusivebreeze.com": "Leven Labs, Inc. DBA Admiral", @@ -50745,6 +51260,7 @@ "encouragingthread.com": "Leven Labs, Inc. DBA Admiral", "endurablebulb.com": "Leven Labs, Inc. DBA Admiral", "energeticladybug.com": "Leven Labs, Inc. DBA Admiral", + "engineertrick.com": "Leven Labs, Inc. DBA Admiral", "enigmaticcanyon.com": "Leven Labs, Inc. DBA Admiral", "enigmaticvoyage.com": "Leven Labs, Inc. DBA Admiral", "enormousearth.com": "Leven Labs, Inc. DBA Admiral", @@ -50760,6 +51276,7 @@ "executeknowledge.com": "Leven Labs, Inc. DBA Admiral", "exhibitsneeze.com": "Leven Labs, Inc. DBA Admiral", "expansioneggnog.com": "Leven Labs, Inc. DBA Admiral", + "exquisiteartisanship.com": "Leven Labs, Inc. DBA Admiral", "exuberantedge.com": "Leven Labs, Inc. DBA Admiral", "fadedsnow.com": "Leven Labs, Inc. DBA Admiral", "fadewaves.com": "Leven Labs, Inc. DBA Admiral", @@ -50783,8 +51300,12 @@ "financefear.com": "Leven Labs, Inc. DBA Admiral", "firstfrogs.com": "Leven Labs, Inc. DBA Admiral", "fixedfold.com": "Leven Labs, Inc. DBA Admiral", + "flameuncle.com": "Leven Labs, Inc. DBA Admiral", "flimsycircle.com": "Leven Labs, Inc. DBA Admiral", "flimsythought.com": "Leven Labs, Inc. DBA Admiral", + "flourishingcollaboration.com": "Leven Labs, Inc. DBA Admiral", + "flourishinginnovation.com": "Leven Labs, Inc. DBA Admiral", + "flourishingpartnership.com": "Leven Labs, Inc. DBA Admiral", "flowerstreatment.com": "Leven Labs, Inc. DBA Admiral", "flowerycreature.com": "Leven Labs, Inc. DBA Admiral", "floweryfact.com": "Leven Labs, Inc. DBA Admiral", @@ -50814,9 +51335,12 @@ "giddycoat.com": "Leven Labs, Inc. DBA Admiral", "giraffepiano.com": "Leven Labs, Inc. DBA Admiral", "givevacation.com": "Leven Labs, Inc. DBA Admiral", + "gladysway.com": "Leven Labs, Inc. DBA Admiral", "gleamingcow.com": "Leven Labs, Inc. DBA Admiral", "glisteningguide.com": "Leven Labs, Inc. DBA Admiral", + "glitteringbrook.com": "Leven Labs, Inc. DBA Admiral", "gloriousbeef.com": "Leven Labs, Inc. DBA Admiral", + "goldfishgrowth.com": "Leven Labs, Inc. DBA Admiral", "gondolagnome.com": "Leven Labs, Inc. DBA Admiral", "gorgeousedge.com": "Leven Labs, Inc. DBA Admiral", "gracefulmilk.com": "Leven Labs, Inc. DBA Admiral", @@ -50867,6 +51391,7 @@ "importantmeat.com": "Leven Labs, Inc. DBA Admiral", "impossibleexpansion.com": "Leven Labs, Inc. DBA Admiral", "impulsejewel.com": "Leven Labs, Inc. DBA Admiral", + "impulselumber.com": "Leven Labs, Inc. DBA Admiral", "incompetentjoke.com": "Leven Labs, Inc. DBA Admiral", "inconclusiveaction.com": "Leven Labs, Inc. DBA Admiral", "inputicicle.com": "Leven Labs, Inc. DBA Admiral", @@ -50881,6 +51406,7 @@ "jubilanttempest.com": "Leven Labs, Inc. DBA Admiral", "jubilantwhisper.com": "Leven Labs, Inc. DBA Admiral", "kaputquill.com": "Leven Labs, Inc. DBA Admiral", + "keenquill.com": "Leven Labs, Inc. DBA Admiral", "knitstamp.com": "Leven Labs, Inc. DBA Admiral", "knottyswing.com": "Leven Labs, Inc. DBA Admiral", "laboredlocket.com": "Leven Labs, Inc. DBA Admiral", @@ -50890,6 +51416,7 @@ "leftliquid.com": "Leven Labs, Inc. DBA Admiral", "liftedknowledge.com": "Leven Labs, Inc. DBA Admiral", "lightenafterthought.com": "Leven Labs, Inc. DBA Admiral", + "lighttalon.com": "Leven Labs, Inc. DBA Admiral", "livelumber.com": "Leven Labs, Inc. DBA Admiral", "livelylaugh.com": "Leven Labs, Inc. DBA Admiral", "livelyreward.com": "Leven Labs, Inc. DBA Admiral", @@ -50909,6 +51436,7 @@ "lunchroomlock.com": "Leven Labs, Inc. DBA Admiral", "lustroushaven.com": "Leven Labs, Inc. DBA Admiral", "maddeningpowder.com": "Leven Labs, Inc. DBA Admiral", + "majesticwaterscape.com": "Leven Labs, Inc. DBA Admiral", "maliciousmusic.com": "Leven Labs, Inc. DBA Admiral", "marketspiders.com": "Leven Labs, Inc. DBA Admiral", "marriedbelief.com": "Leven Labs, Inc. DBA Admiral", @@ -50920,6 +51448,7 @@ "meatydime.com": "Leven Labs, Inc. DBA Admiral", "meddleplant.com": "Leven Labs, Inc. DBA Admiral", "melodiouschorus.com": "Leven Labs, Inc. DBA Admiral", + "melodiouscomposition.com": "Leven Labs, Inc. DBA Admiral", "meltmilk.com": "Leven Labs, Inc. DBA Admiral", "memopilot.com": "Leven Labs, Inc. DBA Admiral", "memorizematch.com": "Leven Labs, Inc. DBA Admiral", @@ -50965,6 +51494,7 @@ "oldfashionedoffer.com": "Leven Labs, Inc. DBA Admiral", "operationchicken.com": "Leven Labs, Inc. DBA Admiral", "optimallimit.com": "Leven Labs, Inc. DBA Admiral", + "opulentsylvan.com": "Leven Labs, Inc. DBA Admiral", "orientedargument.com": "Leven Labs, Inc. DBA Admiral", "outstandingincome.com": "Leven Labs, Inc. DBA Admiral", "outstandingsnails.com": "Leven Labs, Inc. DBA Admiral", @@ -50992,13 +51522,16 @@ "pleasantpump.com": "Leven Labs, Inc. DBA Admiral", "plotrabbit.com": "Leven Labs, Inc. DBA Admiral", "pluckypocket.com": "Leven Labs, Inc. DBA Admiral", + "pluckyzone.com": "Leven Labs, Inc. DBA Admiral", "pocketfaucet.com": "Leven Labs, Inc. DBA Admiral", "poeticpackage.com": "Leven Labs, Inc. DBA Admiral", "pointdigestion.com": "Leven Labs, Inc. DBA Admiral", "pointlesspocket.com": "Leven Labs, Inc. DBA Admiral", "pointlessprofit.com": "Leven Labs, Inc. DBA Admiral", + "polishedfolly.com": "Leven Labs, Inc. DBA Admiral", "politeplanes.com": "Leven Labs, Inc. DBA Admiral", "politicalporter.com": "Leven Labs, Inc. DBA Admiral", + "popplantation.com": "Leven Labs, Inc. DBA Admiral", "possibleboats.com": "Leven Labs, Inc. DBA Admiral", "possiblepencil.com": "Leven Labs, Inc. DBA Admiral", "potatoinvention.com": "Leven Labs, Inc. DBA Admiral", @@ -51014,7 +51547,9 @@ "profusesupport.com": "Leven Labs, Inc. DBA Admiral", "protestcopy.com": "Leven Labs, Inc. DBA Admiral", "psychedelicarithmetic.com": "Leven Labs, Inc. DBA Admiral", + "publicsofa.com": "Leven Labs, Inc. DBA Admiral", "puffypurpose.com": "Leven Labs, Inc. DBA Admiral", + "pulsatingmeadow.com": "Leven Labs, Inc. DBA Admiral", "pumpedpancake.com": "Leven Labs, Inc. DBA Admiral", "punyplant.com": "Leven Labs, Inc. DBA Admiral", "purposepipe.com": "Leven Labs, Inc. DBA Admiral", @@ -51049,6 +51584,8 @@ "regularplants.com": "Leven Labs, Inc. DBA Admiral", "regulatesleet.com": "Leven Labs, Inc. DBA Admiral", "rehabilitatereason.com": "Leven Labs, Inc. DBA Admiral", + "relationrest.com": "Leven Labs, Inc. DBA Admiral", + "rememberdiscussion.com": "Leven Labs, Inc. DBA Admiral", "repeatsweater.com": "Leven Labs, Inc. DBA Admiral", "replaceroute.com": "Leven Labs, Inc. DBA Admiral", "resonantbrush.com": "Leven Labs, Inc. DBA Admiral", @@ -51106,6 +51643,7 @@ "selfishsnake.com": "Leven Labs, Inc. DBA Admiral", "separatesort.com": "Leven Labs, Inc. DBA Admiral", "seraphicjubilee.com": "Leven Labs, Inc. DBA Admiral", + "serenecascade.com": "Leven Labs, Inc. DBA Admiral", "serenepebble.com": "Leven Labs, Inc. DBA Admiral", "serioussuit.com": "Leven Labs, Inc. DBA Admiral", "serpentshampoo.com": "Leven Labs, Inc. DBA Admiral", @@ -51209,6 +51747,7 @@ "stupendoussleet.com": "Leven Labs, Inc. DBA Admiral", "stupendoussnow.com": "Leven Labs, Inc. DBA Admiral", "stupidscene.com": "Leven Labs, Inc. DBA Admiral", + "sublimequartz.com": "Leven Labs, Inc. DBA Admiral", "succeedscene.com": "Leven Labs, Inc. DBA Admiral", "sugarfriction.com": "Leven Labs, Inc. DBA Admiral", "suggestionbridge.com": "Leven Labs, Inc. DBA Admiral", @@ -51230,6 +51769,7 @@ "tangycover.com": "Leven Labs, Inc. DBA Admiral", "tastelesstrees.com": "Leven Labs, Inc. DBA Admiral", "tastelesstrucks.com": "Leven Labs, Inc. DBA Admiral", + "tearfulglass.com": "Leven Labs, Inc. DBA Admiral", "tediousticket.com": "Leven Labs, Inc. DBA Admiral", "teenytinycellar.com": "Leven Labs, Inc. DBA Admiral", "teenytinyshirt.com": "Leven Labs, Inc. DBA Admiral", @@ -51245,6 +51785,7 @@ "thomastorch.com": "Leven Labs, Inc. DBA Admiral", "thoughtlessknot.com": "Leven Labs, Inc. DBA Admiral", "threetruck.com": "Leven Labs, Inc. DBA Admiral", + "thrivingmarketplace.com": "Leven Labs, Inc. DBA Admiral", "ticketaunt.com": "Leven Labs, Inc. DBA Admiral", "tidymitten.com": "Leven Labs, Inc. DBA Admiral", "tiredthroat.com": "Leven Labs, Inc. DBA Admiral", @@ -51293,12 +51834,14 @@ "verdantlabyrinth.com": "Leven Labs, Inc. DBA Admiral", "verdantloom.com": "Leven Labs, Inc. DBA Admiral", "verseballs.com": "Leven Labs, Inc. DBA Admiral", + "vibrantcelebration.com": "Leven Labs, Inc. DBA Admiral", "vibrantgale.com": "Leven Labs, Inc. DBA Admiral", "vibranthaven.com": "Leven Labs, Inc. DBA Admiral", "vibrantpact.com": "Leven Labs, Inc. DBA Admiral", "vibranttalisman.com": "Leven Labs, Inc. DBA Admiral", "virtualvincent.com": "Leven Labs, Inc. DBA Admiral", "vividcanopy.com": "Leven Labs, Inc. DBA Admiral", + "vividfrost.com": "Leven Labs, Inc. DBA Admiral", "vividmeadow.com": "Leven Labs, Inc. DBA Admiral", "vividplume.com": "Leven Labs, Inc. DBA Admiral", "volatileprofit.com": "Leven Labs, Inc. DBA Admiral", @@ -51317,11 +51860,14 @@ "whispermeeting.com": "Leven Labs, Inc. DBA Admiral", "wildcommittee.com": "Leven Labs, Inc. DBA Admiral", "wistfulwaste.com": "Leven Labs, Inc. DBA Admiral", + "wittyshack.com": "Leven Labs, Inc. DBA Admiral", "workoperation.com": "Leven Labs, Inc. DBA Admiral", "wretchedfloor.com": "Leven Labs, Inc. DBA Admiral", "wrongwound.com": "Leven Labs, Inc. DBA Admiral", "zephyrlabyrinth.com": "Leven Labs, Inc. DBA Admiral", "zestycrime.com": "Leven Labs, Inc. DBA Admiral", + "zestyhorizon.com": "Leven Labs, Inc. DBA Admiral", + "zestyrover.com": "Leven Labs, Inc. DBA Admiral", "zipperxray.com": "Leven Labs, Inc. DBA Admiral", "zlp6s.pw": "Leven Labs, Inc. DBA Admiral", "abtasty.com": "AB Tasty", From e05ea229781c7b459116656d5a919e08c4f25375 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 9 Apr 2024 02:19:38 +0000 Subject: [PATCH 033/221] Set marketing version to 1.83.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 4aebe00d0f..71f4ed0cad 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.82.0 +MARKETING_VERSION = 1.83.0 From 0c0e490a1171578204b9068e83ae93e880e03eaa Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 9 Apr 2024 02:29:43 +0000 Subject: [PATCH 034/221] Bump version to 1.83.0 (153) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 92feb2ccd4..06e5c06c5d 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 152 +CURRENT_PROJECT_VERSION = 153 From 101f9408d351a82ed1e86d2ad3b5731bcd0e698b Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Tue, 9 Apr 2024 04:43:49 +0200 Subject: [PATCH 035/221] Update autoconsent to v10.5.0 (#2548) Task/Issue URL: https://app.asana.com/0/1207001382250025/1207001382250025 Autoconsent Release: https://github.com/duckduckgo/autoconsent/releases/tag/v10.5.0 Description Updates Autoconsent to version v10.5.0. --- DuckDuckGo/Autoconsent/autoconsent-bundle.js | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js index db02dcc60b..d7ccb86ad9 100644 --- a/DuckDuckGo/Autoconsent/autoconsent-bundle.js +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -1 +1 @@ -!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||"moove_gdpr_strict_cookies"===e.name||(e.checked=!1)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!1,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="privacy-policy"]'},{click:'div[role="dialog"] button:nth-child(2)'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{click:"#cookiescript_reject"}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www|)?\\.csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{click:'div.b-cookies-informer__switchers > div:nth-child(2) > div[at-attr="checkbox"] > span.b-input-radio__container > input[type="checkbox"]'},{click:"div.b-cookies-informer__nav > button"}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:["#cookie_initial_modal",".modal-backdrop"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:"#cookie_initial_modal"}],detectPopup:[{visible:"#cookie_initial_modal"}],optIn:[{click:"button#jss_consent_all_initial_modal"}],optOut:[{click:"button#jss_open_settings_modal"},{click:"button#jss_consent_checked"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"tumblr-com",cosmetic:!0,prehideSelectors:["#cmp-app-container"],detectCmp:[{exists:"#cmp-app-container"}],detectPopup:[{visible:"#cmp-app-container"}],optIn:[{click:"#tumblr #cmp-app-container div.components-modal__frame > iframe > html body > div > div > div.cmp__dialog-footer > div > button.components-button.white-space-normal.is-primary"}],optOut:[{hide:"#cmp-app-container"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.reduce(((e,t)=>t.prehideSelectors?[...e,...t.prehideSelectors]:e),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); +!function(){"use strict";var e=class e{static setBase(t){e.base=t}static findElement(t,o=null,c=!1){let i=null;return i=null!=o?Array.from(o.querySelectorAll(t.selector)):null!=e.base?Array.from(e.base.querySelectorAll(t.selector)):Array.from(document.querySelectorAll(t.selector)),null!=t.textFilter&&(i=i.filter((e=>{const o=e.textContent.toLowerCase();if(Array.isArray(t.textFilter)){let e=!1;for(const c of t.textFilter)if(-1!==o.indexOf(c.toLowerCase())){e=!0;break}return e}if(null!=t.textFilter)return-1!==o.indexOf(t.textFilter.toLowerCase())}))),null!=t.styleFilters&&(i=i.filter((e=>{const o=window.getComputedStyle(e);let c=!0;for(const e of t.styleFilters){const t=o[e.option];c=e.negated?c&&t!==e.value:c&&t===e.value}return c}))),null!=t.displayFilter&&(i=i.filter((e=>t.displayFilter?0!==e.offsetHeight:0===e.offsetHeight))),null!=t.iframeFilter&&(i=i.filter((()=>t.iframeFilter?window.location!==window.parent.location:window.location===window.parent.location))),null!=t.childFilter&&(i=i.filter((o=>{const c=e.base;e.setBase(o);const i=e.find(t.childFilter);return e.setBase(c),null!=i.target}))),c?i:(i.length>1&&console.warn("Multiple possible targets: ",i,t,o),i[0])}static find(t,o=!1){const c=[];if(null!=t.parent){const i=e.findElement(t.parent,null,o);if(null!=i){if(i instanceof Array)return i.forEach((i=>{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})})),c;{const n=e.findElement(t.target,i,o);n instanceof Array?n.forEach((e=>{c.push({parent:i,target:e})})):c.push({parent:i,target:n})}}}else{const i=e.findElement(t.target,null,o);i instanceof Array?i.forEach((e=>{c.push({parent:null,target:e})})):c.push({parent:null,target:i})}return 0===c.length&&c.push({parent:null,target:null}),o?c:(1!==c.length&&console.warn("Multiple results found, even though multiple false",c),c[0])}};e.base=null;var t=e;function o(e){const o=t.find(e);return"css"===e.type?!!o.target:"checkbox"===e.type?!!o.target&&o.target.checked:void 0}async function c(e,n){switch(e.type){case"click":return async function(e){const o=t.find(e);null!=o.target&&o.target.click();return i(0)}(e);case"list":return async function(e,t){for(const o of e.actions)await c(o,t)}(e,n);case"consent":return async function(e,t){for(const i of e.consents){const e=-1!==t.indexOf(i.type);if(i.matcher&&i.toggleAction){o(i.matcher)!==e&&await c(i.toggleAction)}else e?await c(i.trueAction):await c(i.falseAction)}}(e,n);case"ifcss":return async function(e,o){const i=t.find(e);i.target?e.falseAction&&await c(e.falseAction,o):e.trueAction&&await c(e.trueAction,o)}(e,n);case"waitcss":return async function(e){await new Promise((o=>{let c=e.retries||10;const i=e.waitTime||250,n=()=>{const a=t.find(e);(e.negated&&a.target||!e.negated&&!a.target)&&c>0?(c-=1,setTimeout(n,i)):o()};n()}))}(e);case"foreach":return async function(e,o){const i=t.find(e,!0),n=t.base;for(const n of i)n.target&&(t.setBase(n.target),await c(e.action,o));t.setBase(n)}(e,n);case"hide":return async function(e){const o=t.find(e);o.target&&o.target.classList.add("Autoconsent-Hidden")}(e);case"slide":return async function(e){const o=t.find(e),c=t.find(e.dragTarget);if(o.target){const e=o.target.getBoundingClientRect(),t=c.target.getBoundingClientRect();let i=t.top-e.top,n=t.left-e.left;"y"===this.config.axis.toLowerCase()&&(n=0),"x"===this.config.axis.toLowerCase()&&(i=0);const a=window.screenX+e.left+e.width/2,s=window.screenY+e.top+e.height/2,r=e.left+e.width/2,l=e.top+e.height/2,p=document.createEvent("MouseEvents");p.initMouseEvent("mousedown",!0,!0,window,0,a,s,r,l,!1,!1,!1,!1,0,o.target);const d=document.createEvent("MouseEvents");d.initMouseEvent("mousemove",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target);const u=document.createEvent("MouseEvents");u.initMouseEvent("mouseup",!0,!0,window,0,a+n,s+i,r+n,l+i,!1,!1,!1,!1,0,o.target),o.target.dispatchEvent(p),await this.waitTimeout(10),o.target.dispatchEvent(d),await this.waitTimeout(10),o.target.dispatchEvent(u)}}(e);case"close":return async function(){window.close()}();case"wait":return async function(e){await i(e.waitTime)}(e);case"eval":return async function(e){return console.log("eval!",e.code),new Promise((t=>{try{e.async?(window.eval(e.code),setTimeout((()=>{t(window.eval("window.__consentCheckResult"))}),e.timeout||250)):t(window.eval(e.code))}catch(o){console.warn("eval error",o,e.code),t(!1)}}))}(e);default:throw"Unknown action type: "+e.type}}function i(e){return new Promise((t=>{setTimeout((()=>{t()}),e)}))}function n(){return crypto&&void 0!==crypto.randomUUID?crypto.randomUUID():Math.random().toString()}var a={pending:new Map,sendContentMessage:null};function s(e,t){const o=n();a.sendContentMessage({type:"eval",id:o,code:e,snippetId:t});const c=new class{constructor(e,t=1e3){this.id=e,this.promise=new Promise(((e,t)=>{this.resolve=e,this.reject=t})),this.timer=window.setTimeout((()=>{this.reject(new Error("timeout"))}),t)}}(o);return a.pending.set(c.id,c),c.promise}var r={EVAL_0:()=>console.log(1),EVAL_CONSENTMANAGER_1:()=>window.__cmp&&"object"==typeof __cmp("getCMPData"),EVAL_CONSENTMANAGER_2:()=>!__cmp("consentStatus").userChoiceExists,EVAL_CONSENTMANAGER_3:()=>__cmp("setConsent",0),EVAL_CONSENTMANAGER_4:()=>__cmp("setConsent",1),EVAL_CONSENTMANAGER_5:()=>__cmp("consentStatus").userChoiceExists,EVAL_COOKIEBOT_1:()=>!!window.Cookiebot,EVAL_COOKIEBOT_2:()=>!window.Cookiebot.hasResponse&&!0===window.Cookiebot.dialog?.visible,EVAL_COOKIEBOT_3:()=>window.Cookiebot.withdraw()||!0,EVAL_COOKIEBOT_4:()=>window.Cookiebot.hide()||!0,EVAL_COOKIEBOT_5:()=>!0===window.Cookiebot.declined,EVAL_KLARO_1:()=>{const e=globalThis.klaroConfig||globalThis.klaro?.getManager&&globalThis.klaro.getManager().config;if(!e)return!0;const t=(e.services||e.apps).filter((e=>!e.required)).map((e=>e.name));if(klaro&&klaro.getManager){const e=klaro.getManager();return t.every((t=>!e.consents[t]))}if(klaroConfig&&"cookie"===klaroConfig.storageMethod){const e=klaroConfig.cookieName||klaroConfig.storageName,o=JSON.parse(decodeURIComponent(document.cookie.split(";").find((t=>t.trim().startsWith(e))).split("=")[1]));return Object.keys(o).filter((e=>t.includes(e))).every((e=>!1===o[e]))}},EVAL_ONETRUST_1:()=>window.OnetrustActiveGroups.split(",").filter((e=>e.length>0)).length<=1,EVAL_TRUSTARC_TOP:()=>window&&window.truste&&"0"===window.truste.eu.bindMap.prefCookie,EVAL_ADROLL_0:()=>!document.cookie.includes("__adroll_fpc"),EVAL_ALMACMP_0:()=>document.cookie.includes('"name":"Google","consent":false'),EVAL_AFFINITY_SERIF_COM_0:()=>document.cookie.includes("serif_manage_cookies_viewed")&&!document.cookie.includes("serif_allow_analytics"),EVAL_ARBEITSAGENTUR_TEST:()=>document.cookie.includes("cookie_consent=denied"),EVAL_AXEPTIO_0:()=>document.cookie.includes("axeptio_authorized_vendors=%2C%2C"),EVAL_BAHN_TEST:()=>1===utag.gdpr.getSelectedCategories().length,EVAL_BING_0:()=>document.cookie.includes("AL=0")&&document.cookie.includes("AD=0")&&document.cookie.includes("SM=0"),EVAL_BLOCKSY_0:()=>document.cookie.includes("blocksy_cookies_consent_accepted=no"),EVAL_BORLABS_0:()=>!JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("borlabs-cookie"))).split("=",2)[1])).consents.statistics,EVAL_BUNDESREGIERUNG_DE_0:()=>document.cookie.match("cookie-allow-tracking=0"),EVAL_CANVA_0:()=>!document.cookie.includes("gtm_fpc_engagement_event"),EVAL_CC_BANNER2_0:()=>!!document.cookie.match(/sncc=[^;]+D%3Dtrue/),EVAL_CLICKIO_0:()=>document.cookie.includes("__lxG__consent__v2_daisybit="),EVAL_CLINCH_0:()=>document.cookie.includes("ctc_rejected=1"),EVAL_COOKIECONSENT2_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COOKIECONSENT3_TEST:()=>document.cookie.includes("cc_cookie="),EVAL_COINBASE_0:()=>JSON.parse(decodeURIComponent(document.cookie.match(/cm_(eu|default)_preferences=([0-9a-zA-Z\\{\\}\\[\\]%:]*);?/)[2])).consent.length<=1,EVAL_COMPLIANZ_BANNER_0:()=>document.cookie.includes("cmplz_banner-status=dismissed"),EVAL_COOKIE_LAW_INFO_0:()=>CLI.disableAllCookies()||CLI.reject_close()||!0,EVAL_COOKIE_LAW_INFO_1:()=>-1===document.cookie.indexOf("cookielawinfo-checkbox-non-necessary=yes"),EVAL_COOKIE_LAW_INFO_DETECT:()=>!!window.CLI,EVAL_COOKIE_MANAGER_POPUP_0:()=>!1===JSON.parse(document.cookie.split(";").find((e=>e.trim().startsWith("CookieLevel"))).split("=")[1]).social,EVAL_COOKIEALERT_0:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_1:()=>document.querySelector("body").removeAttribute("style")||!0,EVAL_COOKIEALERT_2:()=>!0===window.CookieConsent.declined,EVAL_COOKIEFIRST_0:()=>{return!1===(e=JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>-1!==e.indexOf("cookiefirst"))).trim()).split("=")[1])).performance&&!1===e.functional&&!1===e.advertising;var e},EVAL_COOKIEFIRST_1:()=>document.querySelectorAll("button[data-cookiefirst-accent-color=true][role=checkbox]:not([disabled])").forEach((e=>"true"==e.getAttribute("aria-checked")&&e.click()))||!0,EVAL_COOKIEINFORMATION_0:()=>CookieInformation.declineAllCategories()||!0,EVAL_COOKIEINFORMATION_1:()=>CookieInformation.submitAllCategories()||!0,EVAL_COOKIEINFORMATION_2:()=>document.cookie.includes("CookieInformationConsent="),EVAL_COOKIEYES_0:()=>document.cookie.includes("advertisement:no"),EVAL_DAILYMOTION_0:()=>!!document.cookie.match("dm-euconsent-v2"),EVAL_DNDBEYOND_TEST:()=>document.cookie.includes("cookie-consent=denied"),EVAL_DSGVO_0:()=>!document.cookie.includes("sp_dsgvo_cookie_settings"),EVAL_DUNELM_0:()=>document.cookie.includes("cc_functional=0")&&document.cookie.includes("cc_targeting=0"),EVAL_ETSY_0:()=>document.querySelectorAll(".gdpr-overlay-body input").forEach((e=>{e.checked=!1}))||!0,EVAL_ETSY_1:()=>document.querySelector(".gdpr-overlay-view button[data-wt-overlay-close]").click()||!0,EVAL_EU_COOKIE_COMPLIANCE_0:()=>-1===document.cookie.indexOf("cookie-agreed=2"),EVAL_EU_COOKIE_LAW_0:()=>!document.cookie.includes("euCookie"),EVAL_EZOIC_0:()=>ezCMP.handleAcceptAllClick(),EVAL_EZOIC_1:()=>!!document.cookie.match(/ez-consent-tcf/),EVAL_GOOGLE_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_HEMA_TEST_0:()=>document.cookie.includes("cookies_rejected=1"),EVAL_IUBENDA_0:()=>document.querySelectorAll(".purposes-item input[type=checkbox]:not([disabled])").forEach((e=>{e.checked&&e.click()}))||!0,EVAL_IUBENDA_1:()=>!!document.cookie.match(/_iub_cs-\d+=/),EVAL_IWINK_TEST:()=>document.cookie.includes("cookie_permission_granted=no"),EVAL_JQUERY_COOKIEBAR_0:()=>!document.cookie.includes("cookies-state=accepted"),EVAL_MEDIAVINE_0:()=>document.querySelectorAll('[data-name="mediavine-gdpr-cmp"] input[type=checkbox]').forEach((e=>e.checked&&e.click()))||!0,EVAL_MICROSOFT_0:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Reject|Ablehnen")))[0].click()||!0,EVAL_MICROSOFT_1:()=>Array.from(document.querySelectorAll("div > button")).filter((e=>e.innerText.match("Accept|Annehmen")))[0].click()||!0,EVAL_MICROSOFT_2:()=>!!document.cookie.match("MSCC|GHCC"),EVAL_MOOVE_0:()=>document.querySelectorAll("#moove_gdpr_cookie_modal input").forEach((e=>{e.disabled||(e.checked="moove_gdpr_strict_cookies"===e.name||"moove_gdpr_strict_cookies"===e.id)}))||!0,EVAL_ONENINETWO_0:()=>document.cookie.includes("CC_ADVERTISING=NO")&&document.cookie.includes("CC_ANALYTICS=NO"),EVAL_OPERA_0:()=>document.cookie.includes("cookie_consent_essential=true")&&!document.cookie.includes("cookie_consent_marketing=true"),EVAL_PAYPAL_0:()=>!0===document.cookie.includes("cookie_prefs"),EVAL_PRIMEBOX_0:()=>!document.cookie.includes("cb-enabled=accepted"),EVAL_PUBTECH_0:()=>document.cookie.includes("euconsent-v2")&&(document.cookie.match(/.YAAAAAAAAAAA/)||document.cookie.match(/.aAAAAAAAAAAA/)||document.cookie.match(/.YAAACFgAAAAA/)),EVAL_REDDIT_0:()=>document.cookie.includes("eu_cookie={%22opted%22:true%2C%22nonessential%22:false}"),EVAL_SIBBO_0:()=>!!window.localStorage.getItem("euconsent-v2"),EVAL_SIRDATA_UNBLOCK_SCROLL:()=>(document.documentElement.classList.forEach((e=>{e.startsWith("sd-cmp-")&&document.documentElement.classList.remove(e)})),!0),EVAL_SNIGEL_0:()=>!!document.cookie.match("snconsent"),EVAL_STEAMPOWERED_0:()=>2===JSON.parse(decodeURIComponent(document.cookie.split(";").find((e=>e.trim().startsWith("cookieSettings"))).split("=")[1])).preference_state,EVAL_SVT_TEST:()=>document.cookie.includes('cookie-consent-1={"optedIn":true,"functionality":false,"statistics":false}'),EVAL_TAKEALOT_0:()=>document.body.classList.remove("freeze")||(document.body.style="")||!0,EVAL_TARTEAUCITRON_0:()=>tarteaucitron.userInterface.respondAll(!1)||!0,EVAL_TARTEAUCITRON_1:()=>tarteaucitron.userInterface.respondAll(!0)||!0,EVAL_TARTEAUCITRON_2:()=>document.cookie.match(/tarteaucitron=[^;]*/)[0].includes("false"),EVAL_TAUNTON_TEST:()=>document.cookie.includes("taunton_user_consent_submitted=true"),EVAL_TEALIUM_0:()=>void 0!==window.utag&&"object"==typeof utag.gdpr,EVAL_TEALIUM_1:()=>utag.gdpr.setConsentValue(!1)||!0,EVAL_TEALIUM_DONOTSELL:()=>utag.gdpr.dns?.setDnsState(!1)||!0,EVAL_TEALIUM_2:()=>utag.gdpr.setConsentValue(!0)||!0,EVAL_TEALIUM_3:()=>1!==utag.gdpr.getConsentState(),EVAL_TEALIUM_DONOTSELL_CHECK:()=>1!==utag.gdpr.dns?.getDnsState(),EVAL_TESTCMP_0:()=>"button_clicked"===window.results.results[0],EVAL_TESTCMP_COSMETIC_0:()=>"banner_hidden"===window.results.results[0],EVAL_THEFREEDICTIONARY_0:()=>cmpUi.showPurposes()||cmpUi.rejectAll()||!0,EVAL_THEFREEDICTIONARY_1:()=>cmpUi.allowAll()||!0,EVAL_THEVERGE_0:()=>document.cookie.includes("_duet_gdpr_acknowledged=1"),EVAL_UBUNTU_COM_0:()=>document.cookie.includes("_cookies_accepted=essential"),EVAL_UK_COOKIE_CONSENT_0:()=>!document.cookie.includes("catAccCookies"),EVAL_USERCENTRICS_API_0:()=>"object"==typeof UC_UI,EVAL_USERCENTRICS_API_1:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_2:()=>!!UC_UI.denyAllConsents(),EVAL_USERCENTRICS_API_3:()=>!!UC_UI.acceptAllConsents(),EVAL_USERCENTRICS_API_4:()=>!!UC_UI.closeCMP(),EVAL_USERCENTRICS_API_5:()=>!0===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_API_6:()=>!1===UC_UI.areAllConsentsAccepted(),EVAL_USERCENTRICS_BUTTON_0:()=>JSON.parse(localStorage.getItem("usercentrics")).consents.every((e=>e.isEssential||!e.consentStatus)),EVAL_WAITROSE_0:()=>Array.from(document.querySelectorAll("label[id$=cookies-deny-label]")).forEach((e=>e.click()))||!0,EVAL_WAITROSE_1:()=>document.cookie.includes("wtr_cookies_advertising=0")&&document.cookie.includes("wtr_cookies_analytics=0"),EVAL_WP_COOKIE_NOTICE_0:()=>document.cookie.includes("wpl_viewed_cookie=no"),EVAL_XE_TEST:()=>document.cookie.includes("xeConsentState={%22performance%22:false%2C%22marketing%22:false%2C%22compliance%22:false}"),EVAL_XING_0:()=>document.cookie.includes("userConsent=%7B%22marketing%22%3Afalse"),EVAL_YOUTUBE_DESKTOP_0:()=>!!document.cookie.match(/SOCS=CAE/),EVAL_YOUTUBE_MOBILE_0:()=>!!document.cookie.match(/SOCS=CAE/)};var l={main:!0,frame:!1,urlPattern:""},p=class{constructor(e){this.runContext=l,this.autoconsent=e}get hasSelfTest(){throw new Error("Not Implemented")}get isIntermediate(){throw new Error("Not Implemented")}get isCosmetic(){throw new Error("Not Implemented")}mainWorldEval(e){const t=r[e];if(!t)return console.warn("Snippet not found",e),Promise.resolve(!1);const o=this.autoconsent.config.logs;if(this.autoconsent.config.isMainWorld){o.evals&&console.log("inline eval:",e,t);let c=!1;try{c=!!t.call(globalThis)}catch(t){o.evals&&console.error("error evaluating rule",e,t)}return Promise.resolve(c)}const c=`(${t.toString()})()`;return o.evals&&console.log("async eval:",e,c),s(c,e).catch((t=>(o.evals&&console.error("error evaluating rule",e,t),!1)))}checkRunContext(){const e={...l,...this.runContext},t=window.top===window;return!(t&&!e.main)&&(!(!t&&!e.frame)&&!(e.urlPattern&&!window.location.href.match(e.urlPattern)))}detectCmp(){throw new Error("Not Implemented")}async detectPopup(){return!1}optOut(){throw new Error("Not Implemented")}optIn(){throw new Error("Not Implemented")}openCmp(){throw new Error("Not Implemented")}async test(){return Promise.resolve(!0)}click(e,t=!1){return this.autoconsent.domActions.click(e,t)}elementExists(e){return this.autoconsent.domActions.elementExists(e)}elementVisible(e,t){return this.autoconsent.domActions.elementVisible(e,t)}waitForElement(e,t){return this.autoconsent.domActions.waitForElement(e,t)}waitForVisible(e,t,o){return this.autoconsent.domActions.waitForVisible(e,t,o)}waitForThenClick(e,t,o){return this.autoconsent.domActions.waitForThenClick(e,t,o)}wait(e){return this.autoconsent.domActions.wait(e)}hide(e,t){return this.autoconsent.domActions.hide(e,t)}prehide(e){return this.autoconsent.domActions.prehide(e)}undoPrehide(){return this.autoconsent.domActions.undoPrehide()}querySingleReplySelector(e,t){return this.autoconsent.domActions.querySingleReplySelector(e,t)}querySelectorChain(e){return this.autoconsent.domActions.querySelectorChain(e)}elementSelector(e){return this.autoconsent.domActions.elementSelector(e)}},d=class extends p{constructor(e,t){super(t),this.rule=e,this.name=e.name,this.runContext=e.runContext||l}get hasSelfTest(){return!!this.rule.test}get isIntermediate(){return!!this.rule.intermediate}get isCosmetic(){return!!this.rule.cosmetic}get prehideSelectors(){return this.rule.prehideSelectors}async detectCmp(){return!!this.rule.detectCmp&&this._runRulesParallel(this.rule.detectCmp)}async detectPopup(){return!!this.rule.detectPopup&&this._runRulesSequentially(this.rule.detectPopup)}async optOut(){const e=this.autoconsent.config.logs;return!!this.rule.optOut&&(e.lifecycle&&console.log("Initiated optOut()",this.rule.optOut),this._runRulesSequentially(this.rule.optOut))}async optIn(){const e=this.autoconsent.config.logs;return!!this.rule.optIn&&(e.lifecycle&&console.log("Initiated optIn()",this.rule.optIn),this._runRulesSequentially(this.rule.optIn))}async openCmp(){return!!this.rule.openCmp&&this._runRulesSequentially(this.rule.openCmp)}async test(){return this.hasSelfTest?this._runRulesSequentially(this.rule.test):super.test()}async evaluateRuleStep(e){const t=[],o=this.autoconsent.config.logs;if(e.exists&&t.push(this.elementExists(e.exists)),e.visible&&t.push(this.elementVisible(e.visible,e.check)),e.eval){const o=this.mainWorldEval(e.eval);t.push(o)}if(e.waitFor&&t.push(this.waitForElement(e.waitFor,e.timeout)),e.waitForVisible&&t.push(this.waitForVisible(e.waitForVisible,e.timeout,e.check)),e.click&&t.push(this.click(e.click,e.all)),e.waitForThenClick&&t.push(this.waitForThenClick(e.waitForThenClick,e.timeout,e.all)),e.wait&&t.push(this.wait(e.wait)),e.hide&&t.push(this.hide(e.hide,e.method)),e.if){if(!e.if.exists&&!e.if.visible)return console.error("invalid conditional rule",e.if),!1;const c=await this.evaluateRuleStep(e.if);o.rulesteps&&console.log("Condition is",c),c?t.push(this._runRulesSequentially(e.then)):e.else?t.push(this._runRulesSequentially(e.else)):t.push(!0)}if(e.any){for(const t of e.any)if(await this.evaluateRuleStep(t))return!0;return!1}if(0===t.length)return o.errors&&console.warn("Unrecognized rule",e),!1;return(await Promise.all(t)).reduce(((e,t)=>e&&t),!0)}async _runRulesParallel(e){const t=e.map((e=>this.evaluateRuleStep(e)));return(await Promise.all(t)).every((e=>!!e))}async _runRulesSequentially(e){const t=this.autoconsent.config.logs;for(const o of e){t.rulesteps&&console.log("Running rule...",o);const e=await this.evaluateRuleStep(o);if(t.rulesteps&&console.log("...rule result",e),!e&&!o.optional)return!1}return!0}};function u(e="autoconsent-css-rules"){const t=`style#${e}`,o=document.querySelector(t);if(o&&o instanceof HTMLStyleElement)return o;{const t=document.head||document.getElementsByTagName("head")[0]||document.documentElement,o=document.createElement("style");return o.id=e,t.appendChild(o),o}}function m(e,t,o="display"){const c=`${t} { ${"opacity"===o?"opacity: 0":"display: none"} !important; z-index: -1 !important; pointer-events: none !important; } `;return e instanceof HTMLStyleElement&&(e.innerText+=c,t.length>0)}async function h(e,t,o){const c=await e();return!c&&t>0?new Promise((c=>{setTimeout((async()=>{c(h(e,t-1,o))}),o)})):Promise.resolve(c)}function k(e){if(!e)return!1;if(null!==e.offsetParent)return!0;{const t=window.getComputedStyle(e);if("fixed"===t.position&&"none"!==t.display)return!0}return!1}function b(e){const t={enabled:!0,autoAction:"optOut",disabledCmps:[],enablePrehide:!0,enableCosmeticRules:!0,detectRetries:20,isMainWorld:!1,prehideTimeout:2e3,logs:{lifecycle:!1,rulesteps:!1,evals:!1,errors:!0,messages:!1}},o=(c=t,globalThis.structuredClone?structuredClone(c):JSON.parse(JSON.stringify(c)));var c;for(const c of Object.keys(t))void 0!==e[c]&&(o[c]=e[c]);return o}var _="#truste-show-consent",g="#truste-consent-track",y=[class extends p{constructor(e){super(e),this.name="TrustArc-top",this.prehideSelectors=[".trustarc-banner-container",`.truste_popframe,.truste_overlay,.truste_box_overlay,${g}`],this.runContext={main:!0,frame:!1},this._shortcutButton=null,this._optInDone=!1}get hasSelfTest(){return!1}get isIntermediate(){return!this._optInDone&&!this._shortcutButton}get isCosmetic(){return!1}async detectCmp(){const e=this.elementExists(`${_},${g}`);return e&&(this._shortcutButton=document.querySelector("#truste-consent-required")),e}async detectPopup(){return this.elementVisible(`#truste-consent-content,#trustarc-banner-overlay,${g}`,"all")}openFrame(){this.click(_)}async optOut(){return this._shortcutButton?(this._shortcutButton.click(),!0):(m(u(),`.truste_popframe, .truste_overlay, .truste_box_overlay, ${g}`),this.click(_),setTimeout((()=>{u().remove()}),1e4),!0)}async optIn(){return this._optInDone=!0,this.click("#truste-consent-button")}async openCmp(){return!0}async test(){return await this.mainWorldEval("EVAL_TRUSTARC_TOP")}},class extends p{constructor(){super(...arguments),this.name="TrustArc-frame",this.runContext={main:!1,frame:!0,urlPattern:"^https://consent-pref\\.trustarc\\.com/\\?"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return!0}async detectPopup(){return this.elementVisible("#defaultpreferencemanager","any")&&this.elementVisible(".mainContent","any")}async navigateToSettings(){return await h((async()=>this.elementExists(".shp")||this.elementVisible(".advance","any")||this.elementExists(".switch span:first-child")),10,500),this.elementExists(".shp")&&this.click(".shp"),await this.waitForElement(".prefPanel",5e3),this.elementVisible(".advance","any")&&this.click(".advance"),await h((()=>this.elementVisible(".switch span:first-child","any")),5,1e3)}async optOut(){return await h((()=>"complete"===document.readyState),20,100),await this.waitForElement(".mainContent[aria-hidden=false]",5e3),!!this.click(".rejectAll")||(this.elementExists(".prefPanel")&&await this.waitForElement('.prefPanel[style="visibility: visible;"]',3e3),this.click("#catDetails0")?(this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",5e3),!0):this.click(".required")?(this.waitForThenClick("#gwt-debug-close_id",5e3),!0):(await this.navigateToSettings(),this.click(".switch span:nth-child(1):not(.active)",!0),this.click(".submit"),this.waitForThenClick("#gwt-debug-close_id",3e5),!0))}async optIn(){return this.click(".call")||(await this.navigateToSettings(),this.click(".switch span:nth-child(2)",!0),this.click(".submit"),this.waitForElement("#gwt-debug-close_id",3e5).then((()=>{this.click("#gwt-debug-close_id")}))),!0}},class extends p{constructor(){super(...arguments),this.name="Cybotcookiebot",this.prehideSelectors=["#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#dtcookie-container,#cookiebanner,#cb-cookieoverlay,.modal--cookie-banner,#cookiebanner_outer,#CookieBanner"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return await this.mainWorldEval("EVAL_COOKIEBOT_1")}async detectPopup(){return this.mainWorldEval("EVAL_COOKIEBOT_2")}async optOut(){await this.wait(500);let e=await this.mainWorldEval("EVAL_COOKIEBOT_3");return await this.wait(500),e=e&&await this.mainWorldEval("EVAL_COOKIEBOT_4"),e}async optIn(){return this.elementExists("#dtcookie-container")?this.click(".h-dtcookie-accept"):(this.click(".CybotCookiebotDialogBodyLevelButton:not(:checked):enabled",!0),this.click("#CybotCookiebotDialogBodyLevelButtonAccept"),this.click("#CybotCookiebotDialogBodyButtonAccept"),!0)}async test(){return await this.wait(500),await this.mainWorldEval("EVAL_COOKIEBOT_5")}},class extends p{constructor(){super(...arguments),this.name="Sourcepoint-frame",this.prehideSelectors=["div[id^='sp_message_container_'],.message-overlay","#sp_privacy_manager_container"],this.ccpaNotice=!1,this.ccpaPopup=!1,this.runContext={main:!0,frame:!0}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){const e=new URL(location.href);return e.searchParams.has("message_id")&&"ccpa-notice.sp-prod.net"===e.hostname?(this.ccpaNotice=!0,!0):"ccpa-pm.sp-prod.net"===e.hostname?(this.ccpaPopup=!0,!0):("/index.html"===e.pathname||"/privacy-manager/index.html"===e.pathname||"/ccpa_pm/index.html"===e.pathname)&&(e.searchParams.has("message_id")||e.searchParams.has("requestUUID")||e.searchParams.has("consentUUID"))}async detectPopup(){return!!this.ccpaNotice||(this.ccpaPopup?await this.waitForElement(".priv-save-btn",2e3):(await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT",2e3),!this.elementExists(".sp_choice_type_9")))}async optIn(){return await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL",2e3),!!this.click(".sp_choice_type_11")||!!this.click(".sp_choice_type_ACCEPT_ALL")}isManagerOpen(){return"/privacy-manager/index.html"===location.pathname||"/ccpa_pm/index.html"===location.pathname}async optOut(){const e=this.autoconsent.config.logs;if(this.ccpaPopup){const e=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.neutral.on .right");for(const t of e)t.click();const t=document.querySelectorAll(".priv-purpose-container .sp-switch-arrow-block a.switch-bg.on");for(const e of t)e.click();return this.click(".priv-save-btn")}if(!this.isManagerOpen()){if(!await this.waitForElement(".sp_choice_type_12,.sp_choice_type_13"))return!1;if(!this.elementExists(".sp_choice_type_12"))return this.click(".sp_choice_type_13");this.click(".sp_choice_type_12"),await h((()=>this.isManagerOpen()),200,100)}await this.waitForElement(".type-modal",2e4),this.waitForThenClick(".ccpa-stack .pm-switch[aria-checked=true] .slider",500,!0);try{const e=".sp_choice_type_REJECT_ALL",t=".reject-toggle",o=await Promise.race([this.waitForElement(e,2e3).then((e=>e?0:-1)),this.waitForElement(t,2e3).then((e=>e?1:-1)),this.waitForElement(".pm-features",2e3).then((e=>e?2:-1))]);if(0===o)return await this.wait(1500),this.click(e);1===o?this.click(t):2===o&&(await this.waitForElement(".pm-features",1e4),this.click(".checked > span",!0),this.click(".chevron"))}catch(t){e.errors&&console.warn(t)}return this.click(".sp_choice_type_SAVE_AND_EXIT")}},class extends p{constructor(){super(...arguments),this.name="consentmanager.net",this.prehideSelectors=["#cmpbox,#cmpbox2"],this.apiAvailable=!1}get hasSelfTest(){return this.apiAvailable}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.apiAvailable=await this.mainWorldEval("EVAL_CONSENTMANAGER_1"),!!this.apiAvailable||this.elementExists("#cmpbox")}async detectPopup(){return this.apiAvailable?(await this.wait(500),await this.mainWorldEval("EVAL_CONSENTMANAGER_2")):this.elementVisible("#cmpbox .cmpmore","any")}async optOut(){return await this.wait(500),this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_3"):!!this.click(".cmpboxbtnno")||(this.elementExists(".cmpwelcomeprpsbtn")?(this.click(".cmpwelcomeprpsbtn > a[aria-checked=true]",!0),this.click(".cmpboxbtnsave"),!0):(this.click(".cmpboxbtncustom"),await this.waitForElement(".cmptblbox",2e3),this.click(".cmptdchoice > a[aria-checked=true]",!0),this.click(".cmpboxbtnyescustomchoices"),this.hide("#cmpwrapper,#cmpbox","display"),!0))}async optIn(){return this.apiAvailable?await this.mainWorldEval("EVAL_CONSENTMANAGER_4"):this.click(".cmpboxbtnyes")}async test(){if(this.apiAvailable)return await this.mainWorldEval("EVAL_CONSENTMANAGER_5")}},class extends p{constructor(){super(...arguments),this.name="Evidon"}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#_evidon_banner")}async detectPopup(){return this.elementVisible("#_evidon_banner","any")}async optOut(){return this.click("#_evidon-decline-button")||(m(u(),"#evidon-prefdiag-overlay,#evidon-prefdiag-background"),this.click("#_evidon-option-button"),await this.waitForElement("#evidon-prefdiag-overlay",5e3),this.click("#evidon-prefdiag-decline")),!0}async optIn(){return this.click("#_evidon-accept-button")}},class extends p{constructor(){super(...arguments),this.name="Onetrust",this.prehideSelectors=["#onetrust-banner-sdk,#onetrust-consent-sdk,.onetrust-pc-dark-filter,.js-consent-banner"],this.runContext={urlPattern:"^(?!.*https://www\\.nba\\.com/)"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("#onetrust-banner-sdk,#onetrust-pc-sdk")}async detectPopup(){return this.elementVisible("#onetrust-banner-sdk,#onetrust-pc-sdk","any")}async optOut(){return this.elementVisible("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies","any")?this.click("#onetrust-reject-all-handler,.ot-pc-refuse-all-handler,.js-reject-cookies"):(this.elementExists("#onetrust-pc-btn-handler")?this.click("#onetrust-pc-btn-handler"):this.click(".ot-sdk-show-settings,button.js-cookie-settings"),await this.waitForElement("#onetrust-consent-sdk",2e3),await this.wait(1e3),this.click("#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked",!0),await this.wait(1e3),await this.waitForElement(".save-preference-btn-handler,.js-consent-save",2e3),this.click(".save-preference-btn-handler,.js-consent-save"),await this.waitForVisible("#onetrust-banner-sdk",5e3,"none"),!0)}async optIn(){return this.click("#onetrust-accept-btn-handler,#accept-recommended-btn-handler,.js-accept-cookies")}async test(){return await h((()=>this.mainWorldEval("EVAL_ONETRUST_1")),10,500)}},class extends p{constructor(){super(...arguments),this.name="Klaro",this.prehideSelectors=[".klaro"],this.settingsOpen=!1}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".klaro > .cookie-modal")?(this.settingsOpen=!0,!0):this.elementExists(".klaro > .cookie-notice")}async detectPopup(){return this.elementVisible(".klaro > .cookie-notice,.klaro > .cookie-modal","any")}async optOut(){return!!this.click(".klaro .cn-decline")||(this.settingsOpen||(this.click(".klaro .cn-learn-more,.klaro .cm-button-manage"),await this.waitForElement(".klaro > .cookie-modal",2e3),this.settingsOpen=!0),!!this.click(".klaro .cn-decline")||(this.click(".cm-purpose:not(.cm-toggle-all) > input:not(.half-checked,.required,.only-required),.cm-purpose:not(.cm-toggle-all) > div > input:not(.half-checked,.required,.only-required)",!0),this.click(".cm-btn-accept,.cm-button")))}async optIn(){return!!this.click(".klaro .cm-btn-accept-all")||(this.settingsOpen?(this.click(".cm-purpose:not(.cm-toggle-all) > input.half-checked",!0),this.click(".cm-btn-accept")):this.click(".klaro .cookie-notice .cm-btn-success"))}async test(){return await this.mainWorldEval("EVAL_KLARO_1")}},class extends p{constructor(){super(...arguments),this.name="Uniconsent"}get prehideSelectors(){return[".unic",".modal:has(.unic)"]}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".unic .unic-box,.unic .unic-bar,.unic .unic-modal")}async detectPopup(){return this.elementVisible(".unic .unic-box,.unic .unic-bar,.unic .unic-modal","any")}async optOut(){if(await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic button").forEach((e=>{const t=e.textContent;(t.includes("Manage Options")||t.includes("Optionen verwalten"))&&e.click()})),await this.waitForElement(".unic input[type=checkbox]",1e3)){await this.waitForElement(".unic button",1e3),document.querySelectorAll(".unic input[type=checkbox]").forEach((e=>{e.checked&&e.click()}));for(const e of document.querySelectorAll(".unic button")){const t=e.textContent;for(const o of["Confirm Choices","Save Choices","Auswahl speichern"])if(t.includes(o))return e.click(),await this.wait(500),!0}}return!1}async optIn(){return this.waitForThenClick(".unic #unic-agree")}async test(){await this.wait(1e3);return!this.elementExists(".unic .unic-box,.unic .unic-bar")}},class extends p{constructor(){super(...arguments),this.prehideSelectors=[".cmp-root"],this.name="Conversant"}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists(".cmp-root .cmp-receptacle")}async detectPopup(){return this.elementVisible(".cmp-root .cmp-receptacle","any")}async optOut(){if(!await this.waitForThenClick(".cmp-main-button:not(.cmp-main-button--primary)"))return!1;if(!await this.waitForElement(".cmp-view-tab-tabs"))return!1;await this.waitForThenClick(".cmp-view-tab-tabs > :first-child"),await this.waitForThenClick(".cmp-view-tab-tabs > .cmp-view-tab--active:first-child");for(const e of Array.from(document.querySelectorAll(".cmp-accordion-item"))){e.querySelector(".cmp-accordion-item-title").click(),await h((()=>!!e.querySelector(".cmp-accordion-item-content.cmp-active")),10,50);const t=e.querySelector(".cmp-accordion-item-content.cmp-active");t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-deny:not(.cmp-toggle-deny--active)").forEach((e=>e.click())),t.querySelectorAll(".cmp-toggle-actions .cmp-toggle-checkbox:not(.cmp-toggle-checkbox--active)").forEach((e=>e.click()))}return await this.click(".cmp-main-button:not(.cmp-main-button--primary)"),!0}async optIn(){return this.waitForThenClick(".cmp-main-button.cmp-main-button--primary")}async test(){return document.cookie.includes("cmp-data=0")}},class extends p{constructor(){super(...arguments),this.name="tiktok.com",this.runContext={urlPattern:"tiktok"}}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}getShadowRoot(){const e=document.querySelector("tiktok-cookie-banner");return e?e.shadowRoot:null}async detectCmp(){return this.elementExists("tiktok-cookie-banner")}async detectPopup(){return k(this.getShadowRoot().querySelector(".tiktok-cookie-banner"))}async optOut(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:first-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no decline button found"),!1)}async optIn(){const e=this.autoconsent.config.logs,t=this.getShadowRoot().querySelector(".button-wrapper button:last-child");return t?(e.rulesteps&&console.log("[clicking]",t),t.click(),!0):(e.errors&&console.log("no accept button found"),!1)}async test(){const e=document.cookie.match(/cookie-consent=([^;]+)/);if(!e)return!1;const t=JSON.parse(decodeURIComponent(e[1]));return Object.values(t).every((e=>"boolean"!=typeof e||!1===e))}},class extends p{constructor(){super(...arguments),this.runContext={urlPattern:"^https://(www\\.)?airbnb\\.[^/]+/"},this.prehideSelectors=["div[data-testid=main-cookies-banner-container]",'div:has(> div:first-child):has(> div:last-child):has(> section [data-testid="strictly-necessary-cookies"])']}get hasSelfTest(){return!0}get isIntermediate(){return!1}get isCosmetic(){return!1}async detectCmp(){return this.elementExists("div[data-testid=main-cookies-banner-container]")}async detectPopup(){return this.elementVisible("div[data-testid=main-cookies-banner-container","any")}async optOut(){let e;for(await this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._snbhip0");e=document.querySelector("[data-testid=modal-container] button[aria-checked=true]:not([disabled])");)e.click();return this.waitForThenClick("button[data-testid=save-btn]")}async optIn(){return this.waitForThenClick("div[data-testid=main-cookies-banner-container] button._148dgdpk")}async test(){return await h((()=>!!document.cookie.match("OptanonAlertBoxClosed")),20,200)}},class extends p{constructor(){super(...arguments),this.name="tumblr-com",this.runContext={urlPattern:"^https://(www\\.)?tumblr\\.com/"}}get hasSelfTest(){return!1}get isIntermediate(){return!1}get isCosmetic(){return!1}get prehideSelectors(){return["#cmp-app-container"]}async detectCmp(){return this.elementExists("#cmp-app-container")}async detectPopup(){return this.elementVisible("#cmp-app-container","any")}async optOut(){let e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary");return!!t&&(t.click(),await h((()=>!!document.querySelector("#cmp-app-container iframe").contentDocument?.querySelector(".cmp__dialog input")),5,500),e=document.querySelector("#cmp-app-container iframe"),t=e.contentDocument?.querySelector(".cmp-components-button.is-secondary"),!!t&&(t.click(),!0))}async optIn(){const e=document.querySelector("#cmp-app-container iframe").contentDocument.querySelector(".cmp-components-button.is-primary");return!!e&&(e.click(),!0)}}];var w=[{name:"192.com",detectCmp:[{exists:".ont-cookies"}],detectPopup:[{visible:".ont-cookies"}],optIn:[{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-ok2"}],optOut:[{click:".ont-cookes-btn-manage"},{click:".ont-btn-main.ont-cookies-btn.js-ont-btn-choose"}],test:[{eval:"EVAL_ONENINETWO_0"}]},{name:"1password-com",cosmetic:!0,prehideSelectors:['footer #footer-root [aria-label="Cookie Consent"]'],detectCmp:[{exists:'footer #footer-root [aria-label="Cookie Consent"]'}],detectPopup:[{visible:'footer #footer-root [aria-label="Cookie Consent"]'}],optIn:[{click:'footer #footer-root [aria-label="Cookie Consent"] button'}],optOut:[{hide:'footer #footer-root [aria-label="Cookie Consent"]'}]},{name:"abconcerts.be",vendorUrl:"https://unknown",intermediate:!1,prehideSelectors:["dialog.cookie-consent"],detectCmp:[{exists:"dialog.cookie-consent form.cookie-consent__form"}],detectPopup:[{visible:"dialog.cookie-consent form.cookie-consent__form"}],optIn:[{waitForThenClick:"dialog.cookie-consent form.cookie-consent__form button[value=yes]"}],optOut:[{if:{exists:"dialog.cookie-consent form.cookie-consent__form button[value=no]"},then:[{click:"dialog.cookie-consent form.cookie-consent__form button[value=no]"}],else:[{click:"dialog.cookie-consent form.cookie-consent__form button.cookie-consent__options-toggle"},{waitForThenClick:'dialog.cookie-consent form.cookie-consent__form button[value="save_options"]'}]}]},{name:"activobank.pt",runContext:{urlPattern:"^https://(www\\.)?activobank\\.pt"},prehideSelectors:["aside#cookies,.overlay-cookies"],detectCmp:[{exists:"#cookies .cookies-btn"}],detectPopup:[{visible:"#cookies #submitCookies"}],optIn:[{waitForThenClick:"#cookies #submitCookies"}],optOut:[{waitForThenClick:"#cookies #rejectCookies"}]},{name:"Adroll",prehideSelectors:["#adroll_consent_container"],detectCmp:[{exists:"#adroll_consent_container"}],detectPopup:[{visible:"#adroll_consent_container"}],optIn:[{waitForThenClick:"#adroll_consent_accept"}],optOut:[{waitForThenClick:"#adroll_consent_reject"}],test:[{eval:"EVAL_ADROLL_0"}]},{name:"affinity.serif.com",detectCmp:[{exists:".c-cookie-banner button[data-qa='allow-all-cookies']"}],detectPopup:[{visible:".c-cookie-banner"}],optIn:[{click:'button[data-qa="allow-all-cookies"]'}],optOut:[{click:'button[data-qa="manage-cookies"]'},{waitFor:'.c-cookie-banner ~ [role="dialog"]'},{waitForThenClick:'.c-cookie-banner ~ [role="dialog"] input[type="checkbox"][value="true"]',all:!0},{click:'.c-cookie-banner ~ [role="dialog"] .c-modal__action button'}],test:[{wait:500},{eval:"EVAL_AFFINITY_SERIF_COM_0"}]},{name:"agolde.com",cosmetic:!0,prehideSelectors:["#modal-1 div[data-micromodal-close]"],detectCmp:[{exists:"#modal-1 div[aria-labelledby=modal-1-title]"}],detectPopup:[{exists:"#modal-1 div[data-micromodal-close]"}],optIn:[{click:'button[aria-label="Close modal"]'}],optOut:[{hide:"#modal-1 div[data-micromodal-close]"}]},{name:"aliexpress",vendorUrl:"https://aliexpress.com/",runContext:{urlPattern:"^https://.*\\.aliexpress\\.com/"},prehideSelectors:["#gdpr-new-container"],detectCmp:[{exists:"#gdpr-new-container"}],detectPopup:[{visible:"#gdpr-new-container"}],optIn:[{waitForThenClick:"#gdpr-new-container .btn-accept"}],optOut:[{waitForThenClick:"#gdpr-new-container .btn-more"},{waitFor:"#gdpr-new-container .gdpr-dialog-switcher"},{click:"#gdpr-new-container .switcher-on",all:!0,optional:!0},{click:"#gdpr-new-container .btn-save"}]},{name:"almacmp",prehideSelectors:["#alma-cmpv2-container"],detectCmp:[{exists:"#alma-cmpv2-container"}],detectPopup:[{visible:"#alma-cmpv2-container #almacmp-modal-layer1"}],optIn:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalConfirmBtn"}],optOut:[{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer1 #almacmp-modalSettingBtn"},{waitFor:"#alma-cmpv2-container #almacmp-modal-layer2"},{waitForThenClick:"#alma-cmpv2-container #almacmp-modal-layer2 #almacmp-reject-all-layer2"}],test:[{eval:"EVAL_ALMACMP_0"}]},{name:"altium.com",cosmetic:!0,prehideSelectors:[".altium-privacy-bar"],detectCmp:[{exists:".altium-privacy-bar"}],detectPopup:[{exists:".altium-privacy-bar"}],optIn:[{click:"a.altium-privacy-bar__btn"}],optOut:[{hide:".altium-privacy-bar"}]},{name:"amazon.com",prehideSelectors:['span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'],detectCmp:[{exists:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],detectPopup:[{visible:'span[data-action="sp-cc"][data-sp-cc*="rejectAllAction"]'}],optIn:[{waitForVisible:"#sp-cc-accept"},{wait:500},{click:"#sp-cc-accept"}],optOut:[{waitForVisible:"#sp-cc-rejectall-link"},{wait:500},{click:"#sp-cc-rejectall-link"}]},{name:"aquasana.com",cosmetic:!0,prehideSelectors:["#consent-tracking"],detectCmp:[{exists:"#consent-tracking"}],detectPopup:[{exists:"#consent-tracking"}],optIn:[{click:"#accept_consent"}],optOut:[{hide:"#consent-tracking"}]},{name:"arbeitsagentur",vendorUrl:"https://www.arbeitsagentur.de/",prehideSelectors:[".modal-open bahf-cookie-disclaimer-dpl3"],detectCmp:[{exists:"bahf-cookie-disclaimer-dpl3"}],detectPopup:[{visible:"bahf-cookie-disclaimer-dpl3"}],optIn:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-primary"]}],optOut:[{waitForThenClick:["bahf-cookie-disclaimer-dpl3","bahf-cd-modal-dpl3 .ba-btn-contrast"]}],test:[{eval:"EVAL_ARBEITSAGENTUR_TEST"}]},{name:"asus",vendorUrl:"https://www.asus.com/",runContext:{urlPattern:"^https://www\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info,#cookie-policy-info-bg"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{waitForThenClick:'#cookie-policy-info [data-agree="Accept Cookies"]'}],optOut:[{if:{exists:"#cookie-policy-info .btn-reject"},then:[{waitForThenClick:"#cookie-policy-info .btn-reject"}],else:[{waitForThenClick:"#cookie-policy-info .btn-setting"},{waitForThenClick:'#cookie-policy-lightbox-wrapper [data-agree="Save Settings"]'}]}]},{name:"athlinks-com",runContext:{urlPattern:"^https://(www\\.)?athlinks\\.com/"},cosmetic:!0,prehideSelectors:["#footer-container ~ div"],detectCmp:[{exists:"#footer-container ~ div"}],detectPopup:[{visible:"#footer-container > div"}],optIn:[{click:"#footer-container ~ div button"}],optOut:[{hide:"#footer-container ~ div"}]},{name:"ausopen.com",cosmetic:!0,detectCmp:[{exists:".gdpr-popup__message"}],detectPopup:[{visible:".gdpr-popup__message"}],optOut:[{hide:".gdpr-popup__message"}],optIn:[{click:".gdpr-popup__message button"}]},{name:"automattic-cmp-optout",prehideSelectors:['form[class*="cookie-banner"][method="post"]'],detectCmp:[{exists:'form[class*="cookie-banner"][method="post"]'}],detectPopup:[{visible:'form[class*="cookie-banner"][method="post"]'}],optIn:[{click:'a[class*="accept-all-button"]'}],optOut:[{click:'form[class*="cookie-banner"] div[class*="simple-options"] a[class*="customize-button"]'},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:'a[class*="accept-selection-button"]'}]},{name:"aws.amazon.com",prehideSelectors:["#awsccc-cb-content","#awsccc-cs-container","#awsccc-cs-modalOverlay","#awsccc-cs-container-inner"],detectCmp:[{exists:"#awsccc-cb-content"}],detectPopup:[{visible:"#awsccc-cb-content"}],optIn:[{click:"button[data-id=awsccc-cb-btn-accept"}],optOut:[{click:"button[data-id=awsccc-cb-btn-customize]"},{waitFor:"input[aria-checked]"},{click:"input[aria-checked=true]",all:!0,optional:!0},{click:"button[data-id=awsccc-cs-btn-save]"}]},{name:"axeptio",prehideSelectors:[".axeptio_widget"],detectCmp:[{exists:".axeptio_widget"}],detectPopup:[{visible:".axeptio_widget"}],optIn:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_acceptAll"}],optOut:[{waitFor:".axeptio-widget--open"},{click:"button#axeptio_btn_dismiss"}],test:[{eval:"EVAL_AXEPTIO_0"}]},{name:"baden-wuerttemberg.de",prehideSelectors:[".cookie-alert.t-dark"],cosmetic:!0,detectCmp:[{exists:".cookie-alert.t-dark"}],detectPopup:[{visible:".cookie-alert.t-dark"}],optIn:[{click:".cookie-alert__form input:not([disabled]):not([checked])"},{click:".cookie-alert__button button"}],optOut:[{hide:".cookie-alert.t-dark"}]},{name:"bahn-de",vendorUrl:"https://www.bahn.de/",cosmetic:!1,runContext:{main:!0,frame:!1,urlPattern:"^https://(www\\.)?bahn\\.de/"},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:["body > div:first-child","#consent-layer"]}],detectPopup:[{visible:["body > div:first-child","#consent-layer"]}],optIn:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-all-cookies"]}],optOut:[{waitForThenClick:["body > div:first-child","#consent-layer .js-accept-essential-cookies"]}],test:[{eval:"EVAL_BAHN_TEST"}]},{name:"bbb.org",runContext:{urlPattern:"^https://www\\.bbb\\.org/"},cosmetic:!0,prehideSelectors:['div[aria-label="use of cookies on bbb.org"]'],detectCmp:[{exists:'div[aria-label="use of cookies on bbb.org"]'}],detectPopup:[{visible:'div[aria-label="use of cookies on bbb.org"]'}],optIn:[{click:'div[aria-label="use of cookies on bbb.org"] button.bds-button-unstyled span.visually-hidden'}],optOut:[{hide:'div[aria-label="use of cookies on bbb.org"]'}]},{name:"bing.com",prehideSelectors:["#bnp_container"],detectCmp:[{exists:"#bnp_cookie_banner"}],detectPopup:[{visible:"#bnp_cookie_banner"}],optIn:[{click:"#bnp_btn_accept"}],optOut:[{click:"#bnp_btn_preference"},{click:"#mcp_savesettings"}],test:[{eval:"EVAL_BING_0"}]},{name:"blocksy",vendorUrl:"https://creativethemes.com/blocksy/docs/extensions/cookies-consent/",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[".cookie-notification"],detectCmp:[{exists:"#blocksy-ext-cookies-consent-styles-css"}],detectPopup:[{visible:".cookie-notification"}],optIn:[{click:".cookie-notification .ct-cookies-decline-button"}],optOut:[{waitForThenClick:".cookie-notification .ct-cookies-decline-button"}],test:[{eval:"EVAL_BLOCKSY_0"}]},{name:"borlabs",detectCmp:[{exists:"._brlbs-block-content"}],detectPopup:[{visible:"._brlbs-bar-wrap,._brlbs-box-wrap"}],optIn:[{click:"a[data-cookie-accept-all]"}],optOut:[{click:"a[data-cookie-individual]"},{waitForVisible:".cookie-preference"},{click:"input[data-borlabs-cookie-checkbox]:checked",all:!0,optional:!0},{click:"#CookiePrefSave"},{wait:500}],prehideSelectors:["#BorlabsCookieBox"],test:[{eval:"EVAL_BORLABS_0"}]},{name:"bundesregierung.de",prehideSelectors:[".bpa-cookie-banner"],detectCmp:[{exists:".bpa-cookie-banner"}],detectPopup:[{visible:".bpa-cookie-banner .bpa-module-full-hero"}],optIn:[{click:".bpa-accept-all-button"}],optOut:[{wait:500,comment:"click is not immediately recognized"},{waitForThenClick:".bpa-close-button"}],test:[{eval:"EVAL_BUNDESREGIERUNG_DE_0"}]},{name:"burpee.com",cosmetic:!0,prehideSelectors:["#notice-cookie-block"],detectCmp:[{exists:"#notice-cookie-block"}],detectPopup:[{exists:"#html-body #notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{hide:"#html-body #notice-cookie-block, #notice-cookie"}]},{name:"canva.com",prehideSelectors:['div[role="dialog"] a[data-anchor-id="cookie-policy"]'],detectCmp:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],detectPopup:[{exists:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'}],optIn:[{click:'div[role="dialog"] button:nth-child(1)'}],optOut:[{if:{exists:'div[role="dialog"] button:nth-child(3)'},then:[{click:'div[role="dialog"] button:nth-child(2)'}],else:[{click:'div[role="dialog"] button:nth-child(2)'},{waitFor:'div[role="dialog"] a[data-anchor-id="cookie-policy"]'},{waitFor:'div[role="dialog"] button[role=switch]'},{click:'div[role="dialog"] button:nth-child(2):not([role])'},{click:'div[role="dialog"] div:last-child button:only-child'}]}],test:[{eval:"EVAL_CANVA_0"}]},{name:"canyon.com",runContext:{urlPattern:"^https://www\\.canyon\\.com/"},prehideSelectors:["div.modal.cookiesModal.is-open"],detectCmp:[{exists:"div.modal.cookiesModal.is-open"}],detectPopup:[{visible:"div.modal.cookiesModal.is-open"}],optIn:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-submit"]'}],optOut:[{click:'div.cookiesModal__buttonWrapper > button[data-closecause="close-by-manage-cookies"]'},{waitForThenClick:"button#js-manage-data-privacy-save-button"}]},{name:"cc-banner-springer",prehideSelectors:[".cc-banner[data-cc-banner]"],detectCmp:[{exists:".cc-banner[data-cc-banner]"}],detectPopup:[{visible:".cc-banner[data-cc-banner]"}],optIn:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=accept]"}],optOut:[{if:{exists:".cc-banner[data-cc-banner] button[data-cc-action=reject]"},then:[{click:".cc-banner[data-cc-banner] button[data-cc-action=reject]"}],else:[{waitForThenClick:".cc-banner[data-cc-banner] button[data-cc-action=preferences]"},{waitFor:".cc-preferences[data-cc-preferences]"},{click:".cc-preferences[data-cc-preferences] input[type=radio][data-cc-action=toggle-category][value=off]",all:!0,optional:!0},{if:{exists:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"},then:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=reject]"}],else:[{click:".cc-preferences[data-cc-preferences] button[data-cc-action=save]"}]}]}],test:[{eval:"EVAL_CC_BANNER2_0"}]},{name:"cc_banner",cosmetic:!0,prehideSelectors:[".cc_banner-wrapper"],detectCmp:[{exists:".cc_banner-wrapper"}],detectPopup:[{visible:".cc_banner"}],optIn:[{click:".cc_btn_accept_all"}],optOut:[{hide:".cc_banner-wrapper"}]},{name:"ciaopeople.it",prehideSelectors:["#cp-gdpr-choices"],detectCmp:[{exists:"#cp-gdpr-choices"}],detectPopup:[{visible:"#cp-gdpr-choices"}],optIn:[{waitForThenClick:".gdpr-btm__right > button:nth-child(2)"}],optOut:[{waitForThenClick:".gdpr-top-content > button"},{waitFor:".gdpr-top-back"},{waitForThenClick:".gdpr-btm__right > button:nth-child(1)"}],test:[{visible:"#cp-gdpr-choices",check:"none"}]},{vendorUrl:"https://www.civicuk.com/cookie-control/",name:"civic-cookie-control",prehideSelectors:["#ccc-module,#ccc-overlay"],detectCmp:[{exists:"#ccc-module"}],detectPopup:[{visible:"#ccc"},{visible:"#ccc-module"}],optOut:[{click:"#ccc-reject-settings"}],optIn:[{click:"#ccc-recommended-settings"}]},{name:"click.io",prehideSelectors:["#cl-consent"],detectCmp:[{exists:"#cl-consent"}],detectPopup:[{visible:"#cl-consent"}],optIn:[{waitForThenClick:'#cl-consent [data-role="b_agree"]'}],optOut:[{waitFor:'#cl-consent [data-role="b_options"]'},{wait:500},{click:'#cl-consent [data-role="b_options"]'},{waitFor:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]'},{click:'.cl-consent-popup.cl-consent-visible [data-role="alloff"]',all:!0},{click:'[data-role="b_save"]'}],test:[{eval:"EVAL_CLICKIO_0",comment:"TODO: this only checks if we interacted at all"}]},{name:"clinch",intermediate:!1,runContext:{frame:!1,main:!0},prehideSelectors:[".consent-modal[role=dialog]"],detectCmp:[{exists:".consent-modal[role=dialog]"}],detectPopup:[{visible:".consent-modal[role=dialog]"}],optIn:[{click:"#consent_agree"}],optOut:[{if:{exists:"#consent_reject"},then:[{click:"#consent_reject"}],else:[{click:"#manage_cookie_preferences"},{click:"#cookie_consent_preferences input:checked",all:!0,optional:!0},{click:"#consent_save"}]}],test:[{eval:"EVAL_CLINCH_0"}]},{name:"clustrmaps.com",runContext:{urlPattern:"^https://(www\\.)?clustrmaps\\.com/"},cosmetic:!0,prehideSelectors:["#gdpr-cookie-message"],detectCmp:[{exists:"#gdpr-cookie-message"}],detectPopup:[{visible:"#gdpr-cookie-message"}],optIn:[{click:"button#gdpr-cookie-accept"}],optOut:[{hide:"#gdpr-cookie-message"}]},{name:"coinbase",intermediate:!1,runContext:{frame:!0,main:!0,urlPattern:"^https://(www|help)\\.coinbase\\.com"},prehideSelectors:[],detectCmp:[{exists:"div[class^=CookieBannerContent__Container]"}],detectPopup:[{visible:"div[class^=CookieBannerContent__Container]"}],optIn:[{click:"div[class^=CookieBannerContent__CTA] :nth-last-child(1)"}],optOut:[{click:"button[class^=CookieBannerContent__Settings]"},{click:"div[class^=CookiePreferencesModal__CategoryContainer] input:checked",all:!0,optional:!0},{click:"div[class^=CookiePreferencesModal__ButtonContainer] > button"}],test:[{eval:"EVAL_COINBASE_0"}]},{name:"Complianz banner",prehideSelectors:["#cmplz-cookiebanner-container"],detectCmp:[{exists:"#cmplz-cookiebanner-container .cmplz-cookiebanner"}],detectPopup:[{visible:"#cmplz-cookiebanner-container .cmplz-cookiebanner",check:"any"}],optIn:[{waitForThenClick:".cmplz-cookiebanner .cmplz-accept"}],optOut:[{waitForThenClick:".cmplz-cookiebanner .cmplz-deny"}],test:[{eval:"EVAL_COMPLIANZ_BANNER_0"}]},{name:"Complianz categories",prehideSelectors:['.cc-type-categories[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-categories[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{exists:'.cc-type-categories[aria-describedby="cookieconsent:desc"] .cc-dismiss'},then:[{click:".cc-dismiss"}],else:[{click:".cc-type-categories input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-save"}]}]},{name:"Complianz notice",prehideSelectors:['.cc-type-info[aria-describedby="cookieconsent:desc"]'],cosmetic:!0,detectCmp:[{exists:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],detectPopup:[{visible:'.cc-type-info[aria-describedby="cookieconsent:desc"] .cc-compliance .cc-btn'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz opt-both",prehideSelectors:['[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"] .cc-type-opt-both'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{waitForThenClick:".cc-deny"}]},{name:"Complianz opt-out",prehideSelectors:['[aria-describedby="cookieconsent:desc"].cc-type-opt-out'],detectCmp:[{exists:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],detectPopup:[{visible:'[aria-describedby="cookieconsent:desc"].cc-type-opt-out'}],optIn:[{click:".cc-accept-all",optional:!0},{click:".cc-allow",optional:!0},{click:".cc-dismiss",optional:!0}],optOut:[{if:{exists:".cc-deny"},then:[{click:".cc-deny"}],else:[{hide:'[aria-describedby="cookieconsent:desc"]'}]}]},{name:"Complianz optin",prehideSelectors:['.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'],detectCmp:[{exists:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],detectPopup:[{visible:'.cc-type-opt-in[aria-describedby="cookieconsent:desc"]'}],optIn:[{any:[{click:".cc-accept-all"},{click:".cc-allow"},{click:".cc-dismiss"}]}],optOut:[{if:{visible:".cc-deny"},then:[{click:".cc-deny"}],else:[{if:{visible:".cc-settings"},then:[{waitForThenClick:".cc-settings"},{waitForVisible:".cc-settings-view"},{click:".cc-settings-view input[type=checkbox]:not([disabled]):checked",all:!0,optional:!0},{click:".cc-settings-view .cc-btn-accept-selected"}],else:[{click:".cc-dismiss"}]}]}]},{name:"cookie-law-info",prehideSelectors:["#cookie-law-info-bar"],detectCmp:[{exists:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_DETECT"}],detectPopup:[{visible:"#cookie-law-info-bar"}],optIn:[{click:'[data-cli_action="accept_all"]'}],optOut:[{hide:"#cookie-law-info-bar"},{eval:"EVAL_COOKIE_LAW_INFO_0"}],test:[{eval:"EVAL_COOKIE_LAW_INFO_1"}]},{name:"cookie-manager-popup",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,detectCmp:[{exists:"#notice-cookie-block #allow-functional-cookies, #notice-cookie-block #btn-cookie-settings"}],detectPopup:[{visible:"#notice-cookie-block"}],optIn:[{click:"#btn-cookie-allow"}],optOut:[{if:{exists:"#allow-functional-cookies"},then:[{click:"#allow-functional-cookies"}],else:[{waitForThenClick:"#btn-cookie-settings"},{waitForVisible:".modal-body"},{click:'.modal-body input:checked, .switch[data-switch="on"]',all:!0,optional:!0},{click:'[role="dialog"] .modal-footer button'}]}],prehideSelectors:["#btn-cookie-settings"],test:[{eval:"EVAL_COOKIE_MANAGER_POPUP_0"}]},{name:"cookie-notice",prehideSelectors:["#cookie-notice"],cosmetic:!0,detectCmp:[{visible:"#cookie-notice .cookie-notice-container"}],detectPopup:[{visible:"#cookie-notice"}],optIn:[{click:"#cn-accept-cookie"}],optOut:[{hide:"#cookie-notice"}]},{name:"cookie-script",vendorUrl:"https://cookie-script.com/",prehideSelectors:["#cookiescript_injected"],detectCmp:[{exists:"#cookiescript_injected"}],detectPopup:[{visible:"#cookiescript_injected"}],optOut:[{if:{exists:"#cookiescript_reject"},then:[{wait:100},{click:"#cookiescript_reject"}],else:[{click:"#cookiescript_manage"},{waitForVisible:".cookiescript_fsd_main"},{waitForThenClick:"#cookiescript_reject"}]}],optIn:[{click:"#cookiescript_accept"}]},{name:"cookieacceptbar",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#cookieAcceptBar.cookieAcceptBar"],detectCmp:[{exists:"#cookieAcceptBar.cookieAcceptBar"}],detectPopup:[{visible:"#cookieAcceptBar.cookieAcceptBar"}],optIn:[{waitForThenClick:"#cookieAcceptBarConfirm"}],optOut:[{hide:"#cookieAcceptBar.cookieAcceptBar"}]},{name:"cookiealert",intermediate:!1,prehideSelectors:[],runContext:{frame:!0,main:!0},detectCmp:[{exists:".cookie-alert-extended"}],detectPopup:[{visible:".cookie-alert-extended-modal"}],optIn:[{click:"button[data-controller='cookie-alert/extended/button/accept']"},{eval:"EVAL_COOKIEALERT_0"}],optOut:[{click:"a[data-controller='cookie-alert/extended/detail-link']"},{click:".cookie-alert-configuration-input:checked",all:!0,optional:!0},{click:"button[data-controller='cookie-alert/extended/button/configuration']"},{eval:"EVAL_COOKIEALERT_0"}],test:[{eval:"EVAL_COOKIEALERT_2"}]},{name:"cookieconsent2",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v2.x.x of the library",prehideSelectors:["#cc--main"],detectCmp:[{exists:"#cc--main"}],detectPopup:[{visible:"#cm"},{exists:"#s-all-bn"}],optIn:[{waitForThenClick:"#s-all-bn"}],optOut:[{waitForThenClick:"#s-rall-bn"}],test:[{eval:"EVAL_COOKIECONSENT2_TEST"}]},{name:"cookieconsent3",vendorUrl:"https://www.github.com/orestbida/cookieconsent",comment:"supports v3.x.x of the library",prehideSelectors:["#cc-main"],detectCmp:[{exists:"#cc-main"}],detectPopup:[{visible:"#cc-main .cm-wrapper"}],optIn:[{waitForThenClick:".cm__btn[data-role=all]"}],optOut:[{waitForThenClick:".cm__btn[data-role=necessary]"}],test:[{eval:"EVAL_COOKIECONSENT3_TEST"}]},{name:"cookiefirst.com",prehideSelectors:["#cookiefirst-root,.cookiefirst-root,[aria-labelledby=cookie-preference-panel-title]"],detectCmp:[{exists:"#cookiefirst-root,.cookiefirst-root"}],detectPopup:[{visible:"#cookiefirst-root,.cookiefirst-root"}],optIn:[{click:"button[data-cookiefirst-action=accept]"}],optOut:[{if:{exists:"button[data-cookiefirst-action=adjust]"},then:[{click:"button[data-cookiefirst-action=adjust]"},{waitForVisible:"[data-cookiefirst-widget=modal]",timeout:1e3},{eval:"EVAL_COOKIEFIRST_1"},{wait:1e3},{click:"button[data-cookiefirst-action=save]"}],else:[{click:"button[data-cookiefirst-action=reject]"}]}],test:[{eval:"EVAL_COOKIEFIRST_0"}]},{name:"Cookie Information Banner",prehideSelectors:["#cookie-information-template-wrapper"],detectCmp:[{exists:"#cookie-information-template-wrapper"}],detectPopup:[{visible:"#cookie-information-template-wrapper"}],optIn:[{eval:"EVAL_COOKIEINFORMATION_1"}],optOut:[{hide:"#cookie-information-template-wrapper",comment:"some templates don't hide the banner automatically"},{eval:"EVAL_COOKIEINFORMATION_0"}],test:[{eval:"EVAL_COOKIEINFORMATION_2"}]},{name:"cookieyes",prehideSelectors:[".cky-overlay,.cky-consent-container"],detectCmp:[{exists:".cky-consent-container"}],detectPopup:[{visible:".cky-consent-container"}],optIn:[{waitForThenClick:".cky-consent-container [data-cky-tag=accept-button]"}],optOut:[{if:{exists:".cky-consent-container [data-cky-tag=reject-button]"},then:[{waitForThenClick:".cky-consent-container [data-cky-tag=reject-button]"}],else:[{if:{exists:".cky-consent-container [data-cky-tag=settings-button]"},then:[{click:".cky-consent-container [data-cky-tag=settings-button]"},{waitFor:".cky-modal-open input[type=checkbox]"},{click:".cky-modal-open input[type=checkbox]:checked",all:!0,optional:!0},{waitForThenClick:".cky-modal [data-cky-tag=detail-save-button]"}],else:[{hide:".cky-consent-container,.cky-overlay"}]}]}],test:[{eval:"EVAL_COOKIEYES_0"}]},{name:"corona-in-zahlen.de",prehideSelectors:[".cookiealert"],detectCmp:[{exists:".cookiealert"}],detectPopup:[{visible:".cookiealert"}],optOut:[{click:".configurecookies"},{click:".confirmcookies"}],optIn:[{click:".acceptcookies"}]},{name:"crossfit-com",cosmetic:!0,prehideSelectors:['body #modal > div > div[class^="_wrapper_"]'],detectCmp:[{exists:'body #modal > div > div[class^="_wrapper_"]'}],detectPopup:[{visible:'body #modal > div > div[class^="_wrapper_"]'}],optIn:[{click:'button[aria-label="accept cookie policy"]'}],optOut:[{hide:'body #modal > div > div[class^="_wrapper_"]'}]},{name:"csu-landtag-de",runContext:{urlPattern:"^https://(www\\.|)?csu-landtag\\.de"},prehideSelectors:["#cookie-disclaimer"],detectCmp:[{exists:"#cookie-disclaimer"}],detectPopup:[{visible:"#cookie-disclaimer"}],optIn:[{click:"#cookieall"}],optOut:[{click:"#cookiesel"}]},{name:"dailymotion-us",cosmetic:!0,prehideSelectors:['div[class*="CookiePopup__desktopContainer"]:has(div[class*="CookiePopup"])'],detectCmp:[{exists:'div[class*="CookiePopup__desktopContainer"]'}],detectPopup:[{visible:'div[class*="CookiePopup__desktopContainer"]'}],optIn:[{click:'div[class*="CookiePopup__desktopContainer"] > button > span'}],optOut:[{hide:'div[class*="CookiePopup__desktopContainer"]'}]},{name:"dailymotion.com",runContext:{urlPattern:"^https://(www\\.)?dailymotion\\.com/"},prehideSelectors:['div[class*="Overlay__container"]:has(div[class*="TCF2Popup"])'],detectCmp:[{exists:'div[class*="TCF2Popup"]'}],detectPopup:[{visible:'[class*="TCF2Popup"] a[href^="https://www.dailymotion.com/legal/cookiemanagement"]'}],optIn:[{waitForThenClick:'button[class*="TCF2Popup__button"]:not([class*="TCF2Popup__personalize"])'}],optOut:[{waitForThenClick:'button[class*="TCF2ContinueWithoutAcceptingButton"]'}],test:[{eval:"EVAL_DAILYMOTION_0"}]},{name:"deepl.com",prehideSelectors:[".dl_cookieBanner_container"],detectCmp:[{exists:".dl_cookieBanner_container"}],detectPopup:[{visible:".dl_cookieBanner_container"}],optOut:[{click:".dl_cookieBanner--buttonSelected"}],optIn:[{click:".dl_cookieBanner--buttonAll"}]},{name:"delta.com",runContext:{urlPattern:"^https://www\\.delta\\.com/"},cosmetic:!0,prehideSelectors:["ngc-cookie-banner"],detectCmp:[{exists:"div.cookie-footer-container"}],detectPopup:[{visible:"div.cookie-footer-container"}],optIn:[{click:" button.cookie-close-icon"}],optOut:[{hide:"div.cookie-footer-container"}]},{name:"dmgmedia-us",prehideSelectors:["#mol-ads-cmp-iframe, div.mol-ads-cmp > form > div"],detectCmp:[{exists:"div.mol-ads-cmp > form > div"}],detectPopup:[{waitForVisible:"div.mol-ads-cmp > form > div"}],optIn:[{waitForThenClick:"button.mol-ads-cmp--btn-primary"}],optOut:[{waitForThenClick:"div.mol-ads-ccpa--message > u > a"},{waitForVisible:".mol-ads-cmp--modal-dialog"},{waitForThenClick:"a.mol-ads-cmp-footer-privacy"},{waitForThenClick:"button.mol-ads-cmp--btn-secondary"}]},{name:"dmgmedia",prehideSelectors:['[data-project="mol-fe-cmp"]'],detectCmp:[{exists:'[data-project="mol-fe-cmp"]'}],detectPopup:[{visible:'[data-project="mol-fe-cmp"]'}],optIn:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=primary]'}],optOut:[{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=basic]'},{waitForVisible:'[data-project="mol-fe-cmp"] div[class*="tabContent"]'},{waitForThenClick:'[data-project="mol-fe-cmp"] div[class*="toggle"][class*="enabled"]',all:!0},{waitForThenClick:'[data-project="mol-fe-cmp"] button[class*=white]'}]},{name:"dndbeyond",vendorUrl:"https://www.dndbeyond.com/",runContext:{urlPattern:"^https://(www\\.)?dndbeyond\\.com/"},prehideSelectors:["[id^=cookie-consent-banner]"],detectCmp:[{exists:"[id^=cookie-consent-banner]"}],detectPopup:[{visible:"[id^=cookie-consent-banner]"}],optIn:[{waitForThenClick:"#cookie-consent-granted"}],optOut:[{waitForThenClick:"#cookie-consent-denied"}],test:[{eval:"EVAL_DNDBEYOND_TEST"}]},{name:"Drupal",detectCmp:[{exists:"#drupalorg-crosssite-gdpr"}],detectPopup:[{visible:"#drupalorg-crosssite-gdpr"}],optOut:[{click:".no"}],optIn:[{click:".yes"}]},{name:"WP DSGVO Tools",link:"https://wordpress.org/plugins/shapepress-dsgvo/",prehideSelectors:[".sp-dsgvo"],cosmetic:!0,detectCmp:[{exists:".sp-dsgvo.sp-dsgvo-popup-overlay"}],detectPopup:[{visible:".sp-dsgvo.sp-dsgvo-popup-overlay",check:"any"}],optIn:[{click:".sp-dsgvo-privacy-btn-accept-all",all:!0}],optOut:[{hide:".sp-dsgvo.sp-dsgvo-popup-overlay"}],test:[{eval:"EVAL_DSGVO_0"}]},{name:"dunelm.com",prehideSelectors:["div[data-testid=cookie-consent-modal-backdrop]"],detectCmp:[{exists:"div[data-testid=cookie-consent-message-contents]"}],detectPopup:[{visible:"div[data-testid=cookie-consent-message-contents]"}],optIn:[{click:'[data-testid="cookie-consent-allow-all"]'}],optOut:[{click:"button[data-testid=cookie-consent-adjust-settings]"},{click:"button[data-testid=cookie-consent-preferences-save]"}],test:[{eval:"EVAL_DUNELM_0"}]},{name:"ecosia",vendorUrl:"https://www.ecosia.org/",runContext:{urlPattern:"^https://www\\.ecosia\\.org/"},prehideSelectors:[".cookie-wrapper"],detectCmp:[{exists:".cookie-wrapper > .cookie-notice"}],detectPopup:[{visible:".cookie-wrapper > .cookie-notice"}],optIn:[{waitForThenClick:"[data-test-id=cookie-notice-accept]"}],optOut:[{waitForThenClick:"[data-test-id=cookie-notice-reject]"}]},{name:"Ensighten ensModal",prehideSelectors:[".ensModal"],detectCmp:[{exists:".ensModal"}],detectPopup:[{visible:".ensModal"}],optIn:[{waitForThenClick:"#modalAcceptButton"}],optOut:[{waitForThenClick:".ensCheckbox:checked",all:!0},{waitForThenClick:"#ensSave"}]},{name:"Ensighten ensNotifyBanner",prehideSelectors:["#ensNotifyBanner"],detectCmp:[{exists:"#ensNotifyBanner"}],detectPopup:[{visible:"#ensNotifyBanner"}],optIn:[{waitForThenClick:"#ensCloseBanner"}],optOut:[{waitForThenClick:"#ensRejectAll,#rejectAll,#ensRejectBanner"}]},{name:"etsy",prehideSelectors:["#gdpr-single-choice-overlay","#gdpr-privacy-settings"],detectCmp:[{exists:"#gdpr-single-choice-overlay"}],detectPopup:[{visible:"#gdpr-single-choice-overlay"}],optOut:[{click:"button[data-gdpr-open-full-settings]"},{waitForVisible:".gdpr-overlay-body input",timeout:3e3},{wait:1e3},{eval:"EVAL_ETSY_0"},{eval:"EVAL_ETSY_1"}],optIn:[{click:"button[data-gdpr-single-choice-accept]"}]},{name:"eu-cookie-compliance-banner",detectCmp:[{exists:"body.eu-cookie-compliance-popup-open"}],detectPopup:[{exists:"body.eu-cookie-compliance-popup-open"}],optIn:[{click:".agree-button"}],optOut:[{if:{visible:".decline-button,.eu-cookie-compliance-save-preferences-button"},then:[{click:".decline-button,.eu-cookie-compliance-save-preferences-button"}]},{hide:".eu-cookie-compliance-banner-info, #sliding-popup"}],test:[{eval:"EVAL_EU_COOKIE_COMPLIANCE_0"}]},{name:"EU Cookie Law",prehideSelectors:[".pea_cook_wrapper,.pea_cook_more_info_popover"],cosmetic:!0,detectCmp:[{exists:".pea_cook_wrapper"}],detectPopup:[{wait:500},{visible:".pea_cook_wrapper"}],optIn:[{click:"#pea_cook_btn"}],optOut:[{hide:".pea_cook_wrapper"}],test:[{eval:"EVAL_EU_COOKIE_LAW_0"}]},{name:"europa-eu",vendorUrl:"https://ec.europa.eu/",runContext:{urlPattern:"^https://[^/]*europa\\.eu/"},prehideSelectors:["#cookie-consent-banner"],detectCmp:[{exists:".cck-container"}],detectPopup:[{visible:".cck-container"}],optIn:[{waitForThenClick:'.cck-actions-button[href="#accept"]'}],optOut:[{waitForThenClick:'.cck-actions-button[href="#refuse"]',hide:".cck-container"}]},{name:"EZoic",prehideSelectors:["#ez-cookie-dialog-wrapper"],detectCmp:[{exists:"#ez-cookie-dialog-wrapper"}],detectPopup:[{visible:"#ez-cookie-dialog-wrapper"}],optIn:[{click:"#ez-accept-all",optional:!0},{eval:"EVAL_EZOIC_0",optional:!0}],optOut:[{wait:500},{click:"#ez-manage-settings"},{waitFor:"#ez-cookie-dialog input[type=checkbox]"},{click:"#ez-cookie-dialog input[type=checkbox]:checked",all:!0},{click:"#ez-save-settings"}],test:[{eval:"EVAL_EZOIC_1"}]},{name:"facebook",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?facebook\\.com/"},prehideSelectors:['div[data-testid="cookie-policy-manage-dialog"]'],detectCmp:[{exists:'div[data-testid="cookie-policy-manage-dialog"]'}],detectPopup:[{visible:'div[data-testid="cookie-policy-manage-dialog"]'}],optIn:[{waitForThenClick:'button[data-cookiebanner="accept_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}],optOut:[{waitForThenClick:'button[data-cookiebanner="accept_only_essential_button"]'},{waitForVisible:'div[data-testid="cookie-policy-manage-dialog"]',check:"none"}]},{name:"fides",vendorUrl:"https://github.com/ethyca/fides",prehideSelectors:["#fides-overlay"],detectCmp:[{exists:"#fides-overlay #fides-banner"}],detectPopup:[{visible:"#fides-overlay #fides-banner"}],optIn:[{waitForThenClick:'#fides-banner [data-testid="Accept all-btn"]'}],optOut:[{waitForThenClick:'#fides-banner [data-testid="Reject all-btn"]'}]},{name:"funding-choices",prehideSelectors:[".fc-consent-root,.fc-dialog-container,.fc-dialog-overlay,.fc-dialog-content"],detectCmp:[{exists:".fc-consent-root"}],detectPopup:[{exists:".fc-dialog-container"}],optOut:[{click:".fc-cta-do-not-consent,.fc-cta-manage-options"},{click:".fc-preference-consent:checked,.fc-preference-legitimate-interest:checked",all:!0,optional:!0},{click:".fc-confirm-choices",optional:!0}],optIn:[{click:".fc-cta-consent"}]},{name:"geeks-for-geeks",runContext:{urlPattern:"^https://www\\.geeksforgeeks\\.org/"},cosmetic:!0,prehideSelectors:[".cookie-consent"],detectCmp:[{exists:".cookie-consent"}],detectPopup:[{visible:".cookie-consent"}],optIn:[{click:".cookie-consent button.consent-btn"}],optOut:[{hide:".cookie-consent"}]},{name:"generic-cosmetic",cosmetic:!0,prehideSelectors:["#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"],detectCmp:[{exists:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],detectPopup:[{visible:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}],optIn:[],optOut:[{hide:"#js-cookie-banner,.js-cookie-banner,.cookie-banner,#cookie-banner"}]},{name:"google-consent-standalone",prehideSelectors:[],detectCmp:[{exists:'a[href^="https://policies.google.com/technologies/cookies"'},{exists:'form[action^="https://consent.google."][action$=".com/save"]'}],detectPopup:[{visible:'a[href^="https://policies.google.com/technologies/cookies"'}],optIn:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=false]) button'}],optOut:[{waitForThenClick:'form[action^="https://consent.google."][action$=".com/save"]:has(input[name=set_eom][value=true]) button'}]},{name:"google.com",prehideSelectors:[".HTjtHe#xe7COe"],detectCmp:[{exists:".HTjtHe#xe7COe"},{exists:'.HTjtHe#xe7COe a[href^="https://policies.google.com/technologies/cookies"]'}],detectPopup:[{visible:".HTjtHe#xe7COe button#W0wltc"}],optIn:[{waitForThenClick:".HTjtHe#xe7COe button#L2AGLb"}],optOut:[{waitForThenClick:".HTjtHe#xe7COe button#W0wltc"}],test:[{eval:"EVAL_GOOGLE_0"}]},{name:"gov.uk",detectCmp:[{exists:"#global-cookie-message"}],detectPopup:[{exists:"#global-cookie-message"}],optIn:[{click:"button[data-accept-cookies=true]"}],optOut:[{click:"button[data-reject-cookies=true],#reject-cookies"},{click:"button[data-hide-cookie-banner=true],#hide-cookie-decision"}]},{name:"hashicorp",vendorUrl:"https://hashicorp.com/",runContext:{urlPattern:"^https://[^.]*\\.hashicorp\\.com/"},prehideSelectors:["[data-testid=consent-banner]"],detectCmp:[{exists:"[data-testid=consent-banner]"}],detectPopup:[{visible:"[data-testid=consent-banner]"}],optIn:[{waitForThenClick:"[data-testid=accept]"}],optOut:[{waitForThenClick:"[data-testid=manage-preferences]"},{waitForThenClick:"[data-testid=consent-mgr-dialog] [data-ga-button=save-preferences]"}]},{name:"healthline-media",prehideSelectors:["#modal-host > div.no-hash > div.window-wrapper"],detectCmp:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],detectPopup:[{exists:"#modal-host > div.no-hash > div.window-wrapper, div[data-testid=qualtrics-container]"}],optIn:[{click:"#modal-host > div.no-hash > div.window-wrapper > div:last-child button"}],optOut:[{if:{exists:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'},then:[{click:'#modal-host > div.no-hash > div.window-wrapper > div:last-child a[href="/privacy-settings"]'}],else:[{waitForVisible:"div#__next"},{click:"#__next div:nth-child(1) > button:first-child"}]}]},{name:"hema",prehideSelectors:[".cookie-modal"],detectCmp:[{visible:".cookie-modal .cookie-accept-btn"}],detectPopup:[{visible:".cookie-modal .cookie-accept-btn"}],optIn:[{waitForThenClick:".cookie-modal .cookie-accept-btn"}],optOut:[{waitForThenClick:".cookie-modal .js-cookie-reject-btn"}],test:[{eval:"EVAL_HEMA_TEST_0"}]},{name:"hetzner.com",runContext:{urlPattern:"^https://www\\.hetzner\\.com/"},prehideSelectors:["#CookieConsent"],detectCmp:[{exists:"#CookieConsent"}],detectPopup:[{visible:"#CookieConsent"}],optIn:[{click:"#CookieConsentGiven"}],optOut:[{click:"#CookieConsentDeclined"}]},{name:"hl.co.uk",prehideSelectors:[".cookieModalContent","#cookie-banner-overlay"],detectCmp:[{exists:"#cookie-banner-overlay"}],detectPopup:[{exists:"#cookie-banner-overlay"}],optIn:[{click:"#acceptCookieButton"}],optOut:[{click:"#manageCookie"},{hide:".cookieSettingsModal"},{waitFor:"#AOCookieToggle"},{click:"#AOCookieToggle[aria-pressed=true]",optional:!0},{waitFor:"#TPCookieToggle"},{click:"#TPCookieToggle[aria-pressed=true]",optional:!0},{click:"#updateCookieButton"}]},{name:"hu-manity",vendorUrl:"https://hu-manity.co/",prehideSelectors:["#hu.hu-wrapper"],detectCmp:[{exists:"#hu.hu-visible"}],detectPopup:[{visible:"#hu.hu-visible"}],optIn:[{waitForThenClick:"[data-hu-action=cookies-notice-consent-choices-3]"},{waitForThenClick:"#hu-cookies-save"}],optOut:[{waitForThenClick:"#hu-cookies-save"}]},{name:"hubspot",detectCmp:[{exists:"#hs-eu-cookie-confirmation"}],detectPopup:[{visible:"#hs-eu-cookie-confirmation"}],optIn:[{click:"#hs-eu-confirmation-button"}],optOut:[{click:"#hs-eu-decline-button"}]},{name:"indeed.com",cosmetic:!0,prehideSelectors:["#CookiePrivacyNotice"],detectCmp:[{exists:"#CookiePrivacyNotice"}],detectPopup:[{visible:"#CookiePrivacyNotice"}],optIn:[{click:"#CookiePrivacyNotice button[data-gnav-element-name=CookiePrivacyNoticeOk]"}],optOut:[{hide:"#CookiePrivacyNotice"}]},{name:"ing.de",runContext:{urlPattern:"^https://www\\.ing\\.de/"},cosmetic:!0,prehideSelectors:['div[slot="backdrop"]'],detectCmp:[{exists:'[data-tag-name="ing-cc-dialog-frame"]'}],detectPopup:[{visible:'[data-tag-name="ing-cc-dialog-frame"]'}],optIn:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="accept"]']}],optOut:[{click:['[data-tag-name="ing-cc-dialog-level0"]','[data-tag-name="ing-cc-button"][class*="more"]']}]},{name:"instagram",vendorUrl:"https://instagram.com",runContext:{urlPattern:"^https://www\\.instagram\\.com/"},prehideSelectors:[".x78zum5.xdt5ytf.xg6iff7.x1n2onr6"],detectCmp:[{exists:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],detectPopup:[{visible:".x1qjc9v5.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xl56j7k"}],optIn:[{waitForThenClick:"._a9--._a9_0"}],optOut:[{waitForThenClick:"._a9--._a9_1"},{wait:2e3}]},{name:"ionos.de",prehideSelectors:[".privacy-consent--backdrop",".privacy-consent--modal"],detectCmp:[{exists:".privacy-consent--modal"}],detectPopup:[{visible:".privacy-consent--modal"}],optIn:[{click:"#selectAll"}],optOut:[{click:".footer-config-link"},{click:"#confirmSelection"}]},{name:"itopvpn.com",cosmetic:!0,prehideSelectors:[".pop-cookie"],detectCmp:[{exists:".pop-cookie"}],detectPopup:[{exists:".pop-cookie"}],optIn:[{click:"#_pcookie"}],optOut:[{hide:".pop-cookie"}]},{name:"iubenda",prehideSelectors:["#iubenda-cs-banner"],detectCmp:[{exists:"#iubenda-cs-banner"}],detectPopup:[{visible:".iubenda-cs-accept-btn"}],optIn:[{click:".iubenda-cs-accept-btn"}],optOut:[{click:".iubenda-cs-customize-btn"},{eval:"EVAL_IUBENDA_0"},{click:"#iubFooterBtn"}],test:[{eval:"EVAL_IUBENDA_1"}]},{name:"iWink",prehideSelectors:["body.cookies-request #cookie-bar"],detectCmp:[{exists:"body.cookies-request #cookie-bar"}],detectPopup:[{visible:"body.cookies-request #cookie-bar"}],optIn:[{waitForThenClick:"body.cookies-request #cookie-bar .allow-cookies"}],optOut:[{waitForThenClick:"body.cookies-request #cookie-bar .disallow-cookies"}],test:[{eval:"EVAL_IWINK_TEST"}]},{name:"jdsports",vendorUrl:"https://www.jdsports.co.uk/",runContext:{urlPattern:"^https://(www|m)\\.jdsports\\."},prehideSelectors:[".miniConsent,#PrivacyPolicyBanner"],detectCmp:[{exists:".miniConsent,#PrivacyPolicyBanner"}],detectPopup:[{visible:".miniConsent,#PrivacyPolicyBanner"}],optIn:[{waitForThenClick:".miniConsent .accept-all-cookies"}],optOut:[{if:{exists:"#PrivacyPolicyBanner"},then:[{hide:"#PrivacyPolicyBanner"}],else:[{waitForThenClick:"#cookie-settings"},{waitForThenClick:"#reject-all-cookies"}]}]},{name:"johnlewis.com",prehideSelectors:["div[class^=pecr-cookie-banner-]"],detectCmp:[{exists:"div[class^=pecr-cookie-banner-]"}],detectPopup:[{exists:"div[class^=pecr-cookie-banner-]"}],optOut:[{click:"button[data-test^=manage-cookies]"},{wait:"500"},{click:"label[data-test^=toggle][class*=checked]:not([class*=disabled])",all:!0,optional:!0},{click:"button[data-test=save-preferences]"}],optIn:[{click:"button[data-test=allow-all]"}]},{name:"jquery.cookieBar",vendorUrl:"https://github.com/kovarp/jquery.cookieBar",prehideSelectors:[".cookie-bar"],cosmetic:!0,detectCmp:[{exists:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons"}],detectPopup:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"any"}],optIn:[{click:".cookie-bar .cookie-bar__btn"}],optOut:[{hide:".cookie-bar"}],test:[{visible:".cookie-bar .cookie-bar__message,.cookie-bar .cookie-bar__buttons",check:"none"},{eval:"EVAL_JQUERY_COOKIEBAR_0"}]},{name:"justwatch.com",prehideSelectors:[".consent-banner"],detectCmp:[{exists:".consent-banner .consent-banner__actions"}],detectPopup:[{visible:".consent-banner .consent-banner__actions"}],optIn:[{click:".consent-banner__actions button.basic-button.primary"}],optOut:[{click:".consent-banner__actions button.basic-button.secondary"},{waitForThenClick:".consent-modal__footer button.basic-button.secondary"},{waitForThenClick:".consent-modal ion-content > div > a:nth-child(9)"},{click:"label.consent-switch input[type=checkbox]:checked",all:!0,optional:!0},{waitForVisible:".consent-modal__footer button.basic-button.primary"},{click:".consent-modal__footer button.basic-button.primary"}]},{name:"ketch",vendorUrl:"https://www.ketch.com",runContext:{frame:!1,main:!0},intermediate:!1,prehideSelectors:["#lanyard_root div[role='dialog']"],detectCmp:[{exists:"#lanyard_root div[role='dialog']"}],detectPopup:[{visible:"#lanyard_root div[role='dialog']"}],optIn:[{if:{exists:"#lanyard_root button[class='confirmButton']"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"},{click:"#lanyard_root button[class='confirmButton']"}],else:[{waitForThenClick:"#lanyard_root div[class*=buttons] > :nth-child(2)"}]}],optOut:[{if:{exists:"#lanyard_root [aria-describedby=banner-description]"},then:[{waitForThenClick:"#lanyard_root div[class*=buttons] > button[class*=secondaryButton]",comment:"can be either settings or reject button"}]},{waitFor:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]",timeout:1e3,optional:!0},{if:{exists:"#lanyard_root [aria-describedby=preference-description],#lanyard_root [aria-describedby=modal-description]"},then:[{waitForThenClick:"#lanyard_root button[class*=rejectButton]"},{click:"#lanyard_root button[class*=confirmButton],#lanyard_root div[class*=actions_] > button:nth-child(1)"}]}]},{name:"kleinanzeigen-de",runContext:{urlPattern:"^https?://(www\\.)?kleinanzeigen\\.de"},prehideSelectors:["#gdpr-banner-container"],detectCmp:[{any:[{exists:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{exists:"#ConsentManagementPage"}]}],detectPopup:[{any:[{visible:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"},{visible:"#ConsentManagementPage"}]}],optIn:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-accept]"}],else:[{click:"#ConsentManagementPage .Button-primary"}]}],optOut:[{if:{exists:"#gdpr-banner-container #gdpr-banner"},then:[{click:"#gdpr-banner-container #gdpr-banner [data-testid=gdpr-banner-cmp-button]"}],else:[{click:"#ConsentManagementPage .Button-secondary"}]}]},{name:"lightbox",prehideSelectors:[".darken-layer.open,.lightbox.lightbox--cookie-consent"],detectCmp:[{exists:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],detectPopup:[{visible:"body.cookie-consent-is-active div.lightbox--cookie-consent > div.lightbox__content > div.cookie-consent[data-jsb]"}],optOut:[{click:".cookie-consent__footer > button[type='submit']:not([data-button='selectAll'])"}],optIn:[{click:".cookie-consent__footer > button[type='submit'][data-button='selectAll']"}]},{name:"lineagrafica",vendorUrl:"https://addons.prestashop.com/en/legal/8734-eu-cookie-law-gdpr-banner-blocker.html",cosmetic:!0,prehideSelectors:["#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"],detectCmp:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],detectPopup:[{exists:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}],optIn:[{waitForThenClick:"#lgcookieslaw_accept"}],optOut:[{hide:"#lgcookieslaw_banner,#lgcookieslaw_modal,.lgcookieslaw-overlay"}]},{name:"linkedin.com",prehideSelectors:[".artdeco-global-alert[type=COOKIE_CONSENT]"],detectCmp:[{exists:".artdeco-global-alert[type=COOKIE_CONSENT]"}],detectPopup:[{visible:".artdeco-global-alert[type=COOKIE_CONSENT]"}],optIn:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=ACCEPT]"}],optOut:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"},{wait:500},{waitForThenClick:".artdeco-global-alert[type=COOKIE_CONSENT] button[action-type=DENY]"}],test:[{waitForVisible:".artdeco-global-alert[type=COOKIE_CONSENT]",check:"none"}]},{name:"livejasmin",vendorUrl:"https://www.livejasmin.com/",runContext:{urlPattern:"^https://(m|www)\\.livejasmin\\.com/"},prehideSelectors:["#consent_modal"],detectCmp:[{exists:"#consent_modal"}],detectPopup:[{visible:"#consent_modal"}],optIn:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:first-of-type"}],optOut:[{waitForThenClick:"#consent_modal button[data-testid=ButtonStyledButton]:nth-of-type(2)"},{waitForVisible:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent]"},{click:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] input[data-testid=PrivacyPreferenceCenterWithConsentCookieSwitch]:checked",optional:!0,all:!0},{waitForThenClick:"[data-testid=PrivacyPreferenceCenterWithConsentCookieContent] button[data-testid=ButtonStyledButton]:last-child"}]},{name:"macpaw.com",cosmetic:!0,prehideSelectors:['div[data-banner="cookies"]'],detectCmp:[{exists:'div[data-banner="cookies"]'}],detectPopup:[{exists:'div[data-banner="cookies"]'}],optIn:[{click:'button[data-banner-close="cookies"]'}],optOut:[{hide:'div[data-banner="cookies"]'}]},{name:"marksandspencer.com",cosmetic:!0,detectCmp:[{exists:".navigation-cookiebbanner"}],detectPopup:[{visible:".navigation-cookiebbanner"}],optOut:[{hide:".navigation-cookiebbanner"}],optIn:[{click:".navigation-cookiebbanner__submit"}]},{name:"mediamarkt.de",prehideSelectors:["div[aria-labelledby=pwa-consent-layer-title]","div[class^=StyledConsentLayerWrapper-]"],detectCmp:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],detectPopup:[{exists:"div[aria-labelledby^=pwa-consent-layer-title]"}],optOut:[{click:"button[data-test^=pwa-consent-layer-deny-all]"}],optIn:[{click:"button[data-test^=pwa-consent-layer-accept-all"}]},{name:"Mediavine",prehideSelectors:['[data-name="mediavine-gdpr-cmp"]'],detectCmp:[{exists:'[data-name="mediavine-gdpr-cmp"]'}],detectPopup:[{wait:500},{visible:'[data-name="mediavine-gdpr-cmp"]'}],optIn:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [format="primary"]'}],optOut:[{waitForThenClick:'[data-name="mediavine-gdpr-cmp"] [data-view="manageSettings"]'},{waitFor:'[data-name="mediavine-gdpr-cmp"] input[type=checkbox]'},{eval:"EVAL_MEDIAVINE_0",optional:!0},{click:'[data-name="mediavine-gdpr-cmp"] [format="secondary"]'}]},{name:"microsoft.com",prehideSelectors:["#wcpConsentBannerCtrl"],detectCmp:[{exists:"#wcpConsentBannerCtrl"}],detectPopup:[{exists:"#wcpConsentBannerCtrl"}],optOut:[{eval:"EVAL_MICROSOFT_0"}],optIn:[{eval:"EVAL_MICROSOFT_1"}],test:[{eval:"EVAL_MICROSOFT_2"}]},{name:"midway-usa",runContext:{urlPattern:"^https://www\\.midwayusa\\.com/"},cosmetic:!0,prehideSelectors:["#cookie-container"],detectCmp:[{exists:['div[aria-label="Cookie Policy Banner"]']}],detectPopup:[{visible:"#cookie-container"}],optIn:[{click:"button#cookie-btn"}],optOut:[{hide:'div[aria-label="Cookie Policy Banner"]'}]},{name:"moneysavingexpert.com",detectCmp:[{exists:"dialog[data-testid=accept-our-cookies-dialog]"}],detectPopup:[{visible:"dialog[data-testid=accept-our-cookies-dialog]"}],optIn:[{click:"#banner-accept"}],optOut:[{click:"#banner-manage"},{click:"#pc-confirm"}]},{name:"monzo.com",prehideSelectors:[".cookie-alert, cookie-alert__content"],detectCmp:[{exists:'div.cookie-alert[role="dialog"]'},{exists:'a[href*="monzo"]'}],detectPopup:[{visible:".cookie-alert__content"}],optIn:[{click:".js-accept-cookie-policy"}],optOut:[{click:".js-decline-cookie-policy"}]},{name:"Moove",prehideSelectors:["#moove_gdpr_cookie_info_bar"],detectCmp:[{exists:"#moove_gdpr_cookie_info_bar"}],detectPopup:[{visible:"#moove_gdpr_cookie_info_bar:not(.moove-gdpr-info-bar-hidden)"}],optIn:[{waitForThenClick:".moove-gdpr-infobar-allow-all"}],optOut:[{if:{exists:"#moove_gdpr_cookie_info_bar .change-settings-button"},then:[{click:"#moove_gdpr_cookie_info_bar .change-settings-button"},{waitForVisible:"#moove_gdpr_cookie_modal"},{eval:"EVAL_MOOVE_0"},{click:".moove-gdpr-modal-save-settings"}],else:[{hide:"#moove_gdpr_cookie_info_bar"}]}],test:[{visible:"#moove_gdpr_cookie_info_bar",check:"none"}]},{name:"national-lottery.co.uk",detectCmp:[{exists:".cuk_cookie_consent"}],detectPopup:[{visible:".cuk_cookie_consent",check:"any"}],optOut:[{click:".cuk_cookie_consent_manage_pref"},{click:".cuk_cookie_consent_save_pref"},{click:".cuk_cookie_consent_close"}],optIn:[{click:".cuk_cookie_consent_accept_all"}]},{name:"nba.com",runContext:{urlPattern:"^https://(www\\.)?nba.com/"},cosmetic:!0,prehideSelectors:["#onetrust-banner-sdk"],detectCmp:[{exists:"#onetrust-banner-sdk"}],detectPopup:[{visible:"#onetrust-banner-sdk"}],optIn:[{click:"#onetrust-accept-btn-handler"}],optOut:[{hide:"#onetrust-banner-sdk"}]},{name:"netflix.de",detectCmp:[{exists:"#cookie-disclosure"}],detectPopup:[{visible:".cookie-disclosure-message",check:"any"}],optIn:[{click:".btn-accept"}],optOut:[{hide:"#cookie-disclosure"},{click:".btn-reject"}]},{name:"nhs.uk",prehideSelectors:["#nhsuk-cookie-banner"],detectCmp:[{exists:"#nhsuk-cookie-banner"}],detectPopup:[{exists:"#nhsuk-cookie-banner"}],optOut:[{click:"#nhsuk-cookie-banner__link_accept"}],optIn:[{click:"#nhsuk-cookie-banner__link_accept_analytics"}]},{name:"notice-cookie",prehideSelectors:[".button--notice"],cosmetic:!0,detectCmp:[{exists:".notice--cookie"}],detectPopup:[{visible:".notice--cookie"}],optIn:[{click:".button--notice"}],optOut:[{hide:".notice--cookie"}]},{name:"nrk.no",cosmetic:!0,prehideSelectors:[".nrk-masthead__info-banner--cookie"],detectCmp:[{exists:".nrk-masthead__info-banner--cookie"}],detectPopup:[{exists:".nrk-masthead__info-banner--cookie"}],optIn:[{click:"div.nrk-masthead__info-banner--cookie button > span:has(+ svg.nrk-close)"}],optOut:[{hide:".nrk-masthead__info-banner--cookie"}]},{name:"obi.de",prehideSelectors:[".disc-cp--active"],detectCmp:[{exists:".disc-cp-modal__modal"}],detectPopup:[{visible:".disc-cp-modal__modal"}],optIn:[{click:".js-disc-cp-accept-all"}],optOut:[{click:".js-disc-cp-deny-all"}]},{name:"om",vendorUrl:"https://olli-machts.de/en/extension/cookie-manager",prehideSelectors:[".tx-om-cookie-consent"],detectCmp:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],detectPopup:[{exists:".tx-om-cookie-consent .active[data-omcookie-panel]"}],optIn:[{waitForThenClick:"[data-omcookie-panel-save=all]"}],optOut:[{if:{exists:"[data-omcookie-panel-save=min]"},then:[{waitForThenClick:"[data-omcookie-panel-save=min]"}],else:[{click:"input[data-omcookie-panel-grp]:checked:not(:disabled)",all:!0,optional:!0},{waitForThenClick:"[data-omcookie-panel-save=save]"}]}]},{name:"onlyFans.com",runContext:{urlPattern:"^https://onlyfans\\.com/"},prehideSelectors:["div.b-cookies-informer"],detectCmp:[{exists:"div.b-cookies-informer"}],detectPopup:[{exists:"div.b-cookies-informer"}],optIn:[{click:"div.b-cookies-informer__nav > button:nth-child(2)"}],optOut:[{click:"div.b-cookies-informer__nav > button:nth-child(1)"},{if:{exists:"div.b-cookies-informer__switchers"},then:[{click:"div.b-cookies-informer__switchers input:not([disabled])",all:!0},{click:"div.b-cookies-informer__nav > button"}]}]},{name:"openli",vendorUrl:"https://openli.com",prehideSelectors:[".legalmonster-cleanslate"],detectCmp:[{exists:".legalmonster-cleanslate"}],detectPopup:[{visible:".legalmonster-cleanslate #lm-cookie-wall-container",check:"any"}],optIn:[{waitForThenClick:"#lm-accept-all"}],optOut:[{waitForThenClick:"#lm-accept-necessary"}]},{name:"opera.com",vendorUrl:"https://unknown",cosmetic:!1,runContext:{main:!0,frame:!1},intermediate:!1,prehideSelectors:[],detectCmp:[{exists:"#cookie-consent .manage-cookies__btn"}],detectPopup:[{visible:"#cookie-consent .cookie-basic-consent__btn"}],optIn:[{waitForThenClick:"#cookie-consent .cookie-basic-consent__btn"}],optOut:[{waitForThenClick:"#cookie-consent .manage-cookies__btn"},{waitForThenClick:"#cookie-consent .active.marketing_option_switch.cookie-consent__switch",all:!0},{waitForThenClick:"#cookie-consent .cookie-selection__btn"}],test:[{eval:"EVAL_OPERA_0"}]},{name:"osano",prehideSelectors:[".osano-cm-window,.osano-cm-dialog"],detectCmp:[{exists:".osano-cm-window"}],detectPopup:[{visible:".osano-cm-dialog"}],optIn:[{click:".osano-cm-accept-all",optional:!0}],optOut:[{waitForThenClick:".osano-cm-denyAll"}]},{name:"otto.de",prehideSelectors:[".cookieBanner--visibility"],detectCmp:[{exists:".cookieBanner--visibility"}],detectPopup:[{visible:".cookieBanner__wrapper"}],optIn:[{click:".js_cookieBannerPermissionButton"}],optOut:[{click:".js_cookieBannerProhibitionButton"}]},{name:"ourworldindata",vendorUrl:"https://ourworldindata.org/",runContext:{urlPattern:"^https://ourworldindata\\.org/"},prehideSelectors:[".cookie-manager"],detectCmp:[{exists:".cookie-manager"}],detectPopup:[{visible:".cookie-manager .cookie-notice.open"}],optIn:[{waitForThenClick:".cookie-notice [data-test=accept]"}],optOut:[{waitForThenClick:".cookie-notice [data-test=reject]"}]},{name:"pabcogypsum",vendorUrl:"https://unknown",prehideSelectors:[".js-cookie-notice:has(#cookie_settings-form)"],detectCmp:[{exists:".js-cookie-notice #cookie_settings-form"}],detectPopup:[{visible:".js-cookie-notice #cookie_settings-form"}],optIn:[{waitForThenClick:".js-cookie-notice button[value=allow]"}],optOut:[{waitForThenClick:".js-cookie-notice button[value=disable]"}]},{name:"paypal-us",prehideSelectors:["#ccpaCookieContent_wrapper, article.ppvx_modal--overpanel"],detectCmp:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],detectPopup:[{exists:"#ccpaCookieBanner, .privacy-sheet-content"}],optIn:[{click:"#acceptAllButton"}],optOut:[{if:{exists:"a#manageCookiesLink"},then:[{click:"a#manageCookiesLink"}],else:[{waitForVisible:".privacy-sheet-content #formContent"},{click:"#formContent .cookiepref-11m2iee-checkbox_base input:checked",all:!0,optional:!0},{click:".confirmCookie #submitCookiesBtn"}]}]},{name:"paypal.com",prehideSelectors:["#gdprCookieBanner"],detectCmp:[{exists:"#gdprCookieBanner"}],detectPopup:[{visible:"#gdprCookieContent_wrapper"}],optIn:[{click:"#acceptAllButton"}],optOut:[{wait:200},{click:".gdprCookieBanner_decline-button"}],test:[{wait:500},{eval:"EVAL_PAYPAL_0"}]},{name:"pinetools.com",cosmetic:!0,prehideSelectors:["#aviso_cookies"],detectCmp:[{exists:"#aviso_cookies"}],detectPopup:[{exists:".lang_en #aviso_cookies"}],optIn:[{click:"#aviso_cookies .a_boton_cerrar"}],optOut:[{hide:"#aviso_cookies"}]},{name:"pmc",cosmetic:!0,prehideSelectors:["#pmc-pp-tou--notice"],detectCmp:[{exists:"#pmc-pp-tou--notice"}],detectPopup:[{visible:"#pmc-pp-tou--notice"}],optIn:[{click:"span.pmc-pp-tou--notice-close-btn"}],optOut:[{hide:"#pmc-pp-tou--notice"}]},{name:"pornhub.com",runContext:{urlPattern:"^https://(www\\.)?pornhub\\.com/"},cosmetic:!0,prehideSelectors:[".cookiesBanner"],detectCmp:[{exists:".cookiesBanner"}],detectPopup:[{visible:".cookiesBanner"}],optIn:[{click:".cookiesBanner .okButton"}],optOut:[{hide:".cookiesBanner"}]},{name:"pornpics.com",cosmetic:!0,prehideSelectors:["#cookie-contract"],detectCmp:[{exists:"#cookie-contract"}],detectPopup:[{visible:"#cookie-contract"}],optIn:[{click:"#cookie-contract .icon-cross"}],optOut:[{hide:"#cookie-contract"}]},{name:"PrimeBox CookieBar",prehideSelectors:["#cookie-bar"],detectCmp:[{exists:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy"}],detectPopup:[{visible:"#cookie-bar .cb-enable,#cookie-bar .cb-disable,#cookie-bar .cb-policy",check:"any"}],optIn:[{waitForThenClick:"#cookie-bar .cb-enable"}],optOut:[{click:"#cookie-bar .cb-disable",optional:!0},{hide:"#cookie-bar"}],test:[{eval:"EVAL_PRIMEBOX_0"}]},{name:"privacymanager.io",prehideSelectors:["#gdpr-consent-tool-wrapper",'iframe[src^="https://cmp-consent-tool.privacymanager.io"]'],runContext:{urlPattern:"^https://cmp-consent-tool\\.privacymanager\\.io/",main:!1,frame:!0},detectCmp:[{exists:"button#save"}],detectPopup:[{visible:"button#save"}],optIn:[{click:"button#save"}],optOut:[{if:{exists:"#denyAll"},then:[{click:"#denyAll"},{waitForThenClick:".okButton"}],else:[{waitForThenClick:"#manageSettings"},{waitFor:".purposes-overview-list"},{waitFor:"button#saveAndExit"},{click:"span[role=checkbox][aria-checked=true]",all:!0,optional:!0},{click:"button#saveAndExit"}]}]},{name:"productz.com",vendorUrl:"https://productz.com/",runContext:{urlPattern:"^https://productz\\.com/"},prehideSelectors:[],detectCmp:[{exists:".c-modal.is-active"}],detectPopup:[{visible:".c-modal.is-active"}],optIn:[{waitForThenClick:".c-modal.is-active .is-accept"}],optOut:[{waitForThenClick:".c-modal.is-active .is-dismiss"}]},{name:"pubtech",prehideSelectors:["#pubtech-cmp"],detectCmp:[{exists:"#pubtech-cmp"}],detectPopup:[{visible:"#pubtech-cmp #pt-actions"}],optIn:[{if:{exists:"#pt-accept-all"},then:[{click:"#pubtech-cmp #pt-actions #pt-accept-all"}],else:[{click:"#pubtech-cmp #pt-actions button:nth-of-type(2)"}]}],optOut:[{click:"#pubtech-cmp #pt-close"}],test:[{eval:"EVAL_PUBTECH_0"}]},{name:"quantcast",prehideSelectors:["#qc-cmp2-main,#qc-cmp2-container"],detectCmp:[{exists:"#qc-cmp2-container"}],detectPopup:[{visible:"#qc-cmp2-ui"}],optOut:[{click:'.qc-cmp2-summary-buttons > button[mode="secondary"]'},{waitFor:"#qc-cmp2-ui"},{click:'.qc-cmp2-toggle-switch > button[aria-checked="true"]',all:!0,optional:!0},{click:'.qc-cmp2-main button[aria-label="REJECT ALL"]',optional:!0},{waitForThenClick:'.qc-cmp2-main button[aria-label="SAVE & EXIT"],.qc-cmp2-buttons-desktop > button[mode="primary"]',timeout:5e3}],optIn:[{click:'.qc-cmp2-summary-buttons > button[mode="primary"]'}]},{name:"reddit.com",runContext:{urlPattern:"^https://www\\.reddit\\.com/"},prehideSelectors:["[bundlename=reddit_cookie_banner]"],detectCmp:[{exists:"reddit-cookie-banner"}],detectPopup:[{visible:"reddit-cookie-banner"}],optIn:[{waitForThenClick:["reddit-cookie-banner","#accept-all-cookies-button > button"]}],optOut:[{waitForThenClick:["reddit-cookie-banner","#reject-nonessential-cookies-button > button"]}],test:[{eval:"EVAL_REDDIT_0"}]},{name:"rog-forum.asus.com",runContext:{urlPattern:"^https://rog-forum\\.asus\\.com/"},prehideSelectors:["#cookie-policy-info"],detectCmp:[{exists:"#cookie-policy-info"}],detectPopup:[{visible:"#cookie-policy-info"}],optIn:[{click:'div.cookie-btn-box > div[aria-label="Accept"]'}],optOut:[{click:'div.cookie-btn-box > div[aria-label="Reject"]'},{waitForThenClick:'.cookie-policy-lightbox-bottom > div[aria-label="Save Settings"]'}]},{name:"roofingmegastore.co.uk",runContext:{urlPattern:"^https://(www\\.)?roofingmegastore\\.co\\.uk"},prehideSelectors:["#m-cookienotice"],detectCmp:[{exists:"#m-cookienotice"}],detectPopup:[{visible:"#m-cookienotice"}],optIn:[{click:"#accept-cookies"}],optOut:[{click:"#manage-cookies"},{waitForThenClick:"#accept-selected"}]},{name:"samsung.com",runContext:{urlPattern:"^https://www\\.samsung\\.com/"},cosmetic:!0,prehideSelectors:["div.cookie-bar"],detectCmp:[{exists:"div.cookie-bar"}],detectPopup:[{visible:"div.cookie-bar"}],optIn:[{click:"div.cookie-bar__manage > a"}],optOut:[{hide:"div.cookie-bar"}]},{name:"setapp.com",vendorUrl:"https://setapp.com/",cosmetic:!0,runContext:{urlPattern:"^https://setapp\\.com/"},prehideSelectors:[],detectCmp:[{exists:".cookie-banner.js-cookie-banner"}],detectPopup:[{visible:".cookie-banner.js-cookie-banner"}],optIn:[{waitForThenClick:".cookie-banner.js-cookie-banner button"}],optOut:[{hide:".cookie-banner.js-cookie-banner"}]},{name:"sibbo",prehideSelectors:["sibbo-cmp-layout"],detectCmp:[{exists:"sibbo-cmp-layout"}],detectPopup:[{visible:"sibbo-cmp-layout"}],optIn:[{click:"sibbo-cmp-layout [data-accept-all]"}],optOut:[{click:'.sibbo-panel__aside__buttons a[data-nav="purposes"]'},{click:'.sibbo-panel__main__header__actions a[data-focusable="reject-all"]'},{if:{exists:"[data-view=purposes] .sibbo-panel__main__footer__actions [data-save-and-exit]"},then:[],else:[{waitFor:'.sibbo-panel__main__footer__actions a[data-focusable="next"]:not(.sibbo-cmp-button--disabled)'},{click:'.sibbo-panel__main__footer__actions a[data-focusable="next"]'},{click:'.sibbo-panel__main div[data-view="purposesLegInt"] a[data-focusable="reject-all"]'}]},{waitFor:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"},{click:".sibbo-panel__main__footer__actions [data-save-and-exit]:not(.sibbo-cmp-button--disabled)"}],test:[{eval:"EVAL_SIBBO_0"}]},{name:"similarweb.com",cosmetic:!0,prehideSelectors:[".app-cookies-notification"],detectCmp:[{exists:".app-cookies-notification"}],detectPopup:[{exists:".app-layout .app-cookies-notification"}],optIn:[{click:"button.app-cookies-notification__dismiss"}],optOut:[{hide:".app-layout .app-cookies-notification"}]},{name:"Sirdata",cosmetic:!1,prehideSelectors:["#sd-cmp"],detectCmp:[{exists:"#sd-cmp"}],detectPopup:[{visible:"#sd-cmp"}],optIn:[{waitForThenClick:"#sd-cmp .sd-cmp-3cRQ2"}],optOut:[{waitForThenClick:["#sd-cmp","xpath///span[contains(., 'Do not accept') or contains(., 'Acceptera inte') or contains(., 'No aceptar') or contains(., 'Ikke acceptere') or contains(., 'Nicht akzeptieren') or contains(., 'Не приемам') or contains(., 'Να μην γίνει αποδοχή') or contains(., 'Niet accepteren') or contains(., 'Nepřijímat') or contains(., 'Nie akceptuj') or contains(., 'Nu acceptați') or contains(., 'Não aceitar') or contains(., 'Continuer sans accepter') or contains(., 'Non accettare') or contains(., 'Nem fogad el')]"]}]},{name:"snigel",detectCmp:[{exists:".snigel-cmp-framework"}],detectPopup:[{visible:".snigel-cmp-framework"}],optOut:[{click:"#sn-b-custom"},{click:"#sn-b-save"}],test:[{eval:"EVAL_SNIGEL_0"}],optIn:[{click:".snigel-cmp-framework #accept-choices"}]},{name:"steampowered.com",detectCmp:[{exists:".cookiepreferences_popup"},{visible:".cookiepreferences_popup"}],detectPopup:[{visible:".cookiepreferences_popup"}],optOut:[{click:"#rejectAllButton"}],optIn:[{click:"#acceptAllButton"}],test:[{wait:1e3},{eval:"EVAL_STEAMPOWERED_0"}]},{name:"strato.de",prehideSelectors:[".consent__wrapper"],runContext:{urlPattern:"^https://www\\.strato\\.de/"},detectCmp:[{exists:".consent"}],detectPopup:[{visible:".consent"}],optIn:[{click:"button.consentAgree"}],optOut:[{click:"button.consentSettings"},{waitForThenClick:"button#consentSubmit"}]},{name:"svt.se",vendorUrl:"https://www.svt.se/",runContext:{urlPattern:"^https://www\\.svt\\.se/"},prehideSelectors:["[class*=CookieConsent__root___]"],detectCmp:[{exists:"[class*=CookieConsent__root___]"}],detectPopup:[{visible:"[class*=CookieConsent__modal___]"}],optIn:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=primary]"}],optOut:[{waitForThenClick:"[class*=CookieConsent__modal___] > div > button[class*=secondary]:nth-child(2)"}],test:[{eval:"EVAL_SVT_TEST"}]},{name:"takealot.com",cosmetic:!0,prehideSelectors:['div[class^="cookies-banner-module_"]'],detectCmp:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],detectPopup:[{exists:'div[class^="cookies-banner-module_cookie-banner_"]'}],optIn:[{click:'button[class*="cookies-banner-module_dismiss-button_"]'}],optOut:[{hide:'div[class^="cookies-banner-module_"]'},{if:{exists:'div[class^="cookies-banner-module_small-cookie-banner_"]'},then:[{eval:"EVAL_TAKEALOT_0"}],else:[]}]},{name:"tarteaucitron.js",prehideSelectors:["#tarteaucitronRoot"],detectCmp:[{exists:"#tarteaucitronRoot"}],detectPopup:[{visible:"#tarteaucitronRoot #tarteaucitronAlertSmall,#tarteaucitronRoot #tarteaucitronAlertBig",check:"any"}],optIn:[{eval:"EVAL_TARTEAUCITRON_1"}],optOut:[{eval:"EVAL_TARTEAUCITRON_0"}],test:[{eval:"EVAL_TARTEAUCITRON_2",comment:"sometimes there are required categories, so we check that at least something is false"}]},{name:"taunton",vendorUrl:"https://www.taunton.com/",prehideSelectors:["#taunton-user-consent__overlay"],detectCmp:[{exists:"#taunton-user-consent__overlay"}],detectPopup:[{exists:"#taunton-user-consent__overlay:not([aria-hidden=true])"}],optIn:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:not(:checked)"},{click:"#taunton-user-consent__toolbar button[type=submit]"}],optOut:[{click:"#taunton-user-consent__toolbar input[type=checkbox]:checked",optional:!0,all:!0},{click:"#taunton-user-consent__toolbar button[type=submit]"}],test:[{eval:"EVAL_TAUNTON_TEST"}]},{name:"Tealium",prehideSelectors:["#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal,#consent-layer"],detectCmp:[{exists:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *"},{eval:"EVAL_TEALIUM_0"}],detectPopup:[{visible:"#__tealiumGDPRecModal *,#__tealiumGDPRcpPrefs *,#__tealiumImplicitmodal *",check:"any"}],optOut:[{eval:"EVAL_TEALIUM_1"},{eval:"EVAL_TEALIUM_DONOTSELL"},{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#__tealiumImplicitmodal"},{waitForThenClick:"#cm-acceptNone,.js-accept-essential-cookies",timeout:1e3,optional:!0}],optIn:[{hide:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs"},{eval:"EVAL_TEALIUM_2"}],test:[{eval:"EVAL_TEALIUM_3"},{eval:"EVAL_TEALIUM_DONOTSELL_CHECK"},{visible:"#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs",check:"none"}]},{name:"temu",vendorUrl:"https://temu.com",runContext:{urlPattern:"^https://[^/]*temu\\.com/"},prehideSelectors:["._2d-8vq-W,._1UdBUwni"],detectCmp:[{exists:"._3YCsmIaS"}],detectPopup:[{visible:"._3YCsmIaS"}],optIn:[{waitForThenClick:"._3fKiu5wx._3zN5SumS._3tAK973O.IYOfhWEs.VGNGF1pA"}],optOut:[{waitForThenClick:"._3fKiu5wx._1_XToJBF._3tAK973O.IYOfhWEs.VGNGF1pA"}]},{name:"Termly",prehideSelectors:["#termly-code-snippet-support"],detectCmp:[{exists:"#termly-code-snippet-support"}],detectPopup:[{visible:"#termly-code-snippet-support div"}],optIn:[{waitForThenClick:'[data-tid="banner-accept"]'}],optOut:[{if:{exists:'[data-tid="banner-decline"]'},then:[{click:'[data-tid="banner-decline"]'}],else:[{click:".t-preference-button"},{wait:500},{if:{exists:".t-declineAllButton"},then:[{click:".t-declineAllButton"}],else:[{waitForThenClick:".t-preference-modal input[type=checkbox][checked]:not([disabled])",all:!0},{waitForThenClick:".t-saveButton"}]}]}]},{name:"termsfeed",vendorUrl:"https://termsfeed.com",comment:"v4.x.x",prehideSelectors:[".termsfeed-com---nb"],detectCmp:[{exists:".termsfeed-com---nb"}],detectPopup:[{visible:".termsfeed-com---nb"}],optIn:[{waitForThenClick:".cc-nb-okagree"}],optOut:[{waitForThenClick:".cc-nb-reject"}]},{name:"termsfeed3",vendorUrl:"https://termsfeed.com",comment:"v3.x.x",cosmetic:!0,prehideSelectors:[".cc_dialog.cc_css_reboot"],detectCmp:[{exists:".cc_dialog.cc_css_reboot"}],detectPopup:[{visible:".cc_dialog.cc_css_reboot"}],optIn:[{waitForThenClick:".cc_dialog.cc_css_reboot .cc_b_ok"}],optOut:[{hide:".cc_dialog.cc_css_reboot"}]},{name:"Test page cosmetic CMP",cosmetic:!0,prehideSelectors:["#privacy-test-page-cmp-test-prehide"],detectCmp:[{exists:"#privacy-test-page-cmp-test-banner"}],detectPopup:[{visible:"#privacy-test-page-cmp-test-banner"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{hide:"#privacy-test-page-cmp-test-banner"}],test:[{wait:500},{eval:"EVAL_TESTCMP_COSMETIC_0"}]},{name:"Test page CMP",prehideSelectors:["#reject-all"],detectCmp:[{exists:"#privacy-test-page-cmp-test"}],detectPopup:[{visible:"#privacy-test-page-cmp-test"}],optIn:[{waitFor:"#accept-all"},{click:"#accept-all"}],optOut:[{waitFor:"#reject-all"},{click:"#reject-all"}],test:[{eval:"EVAL_TESTCMP_0"}]},{name:"thalia.de",prehideSelectors:[".consent-banner-box"],detectCmp:[{exists:"consent-banner[component=consent-banner]"}],detectPopup:[{visible:".consent-banner-box"}],optIn:[{click:".button-zustimmen"}],optOut:[{click:"button[data-consent=disagree]"}]},{name:"thefreedictionary.com",prehideSelectors:["#cmpBanner"],detectCmp:[{exists:"#cmpBanner"}],detectPopup:[{visible:"#cmpBanner"}],optIn:[{eval:"EVAL_THEFREEDICTIONARY_1"}],optOut:[{eval:"EVAL_THEFREEDICTIONARY_0"}]},{name:"theverge",runContext:{frame:!1,main:!0,urlPattern:"^https://(www)?\\.theverge\\.com"},intermediate:!1,prehideSelectors:[".duet--cta--cookie-banner"],detectCmp:[{exists:".duet--cta--cookie-banner"}],detectPopup:[{visible:".duet--cta--cookie-banner"}],optIn:[{click:".duet--cta--cookie-banner button.tracking-12",all:!1}],optOut:[{click:".duet--cta--cookie-banner button.tracking-12 > span"}],test:[{eval:"EVAL_THEVERGE_0"}]},{name:"tidbits-com",cosmetic:!0,prehideSelectors:["#eu_cookie_law_widget-2"],detectCmp:[{exists:"#eu_cookie_law_widget-2"}],detectPopup:[{visible:"#eu_cookie_law_widget-2"}],optIn:[{click:"#eu-cookie-law form > input.accept"}],optOut:[{hide:"#eu_cookie_law_widget-2"}]},{name:"tractor-supply",runContext:{urlPattern:"^https://www\\.tractorsupply\\.com/"},cosmetic:!0,prehideSelectors:[".tsc-cookie-banner"],detectCmp:[{exists:".tsc-cookie-banner"}],detectPopup:[{visible:".tsc-cookie-banner"}],optIn:[{click:"#cookie-banner-cancel"}],optOut:[{hide:".tsc-cookie-banner"}]},{name:"trader-joes-com",cosmetic:!0,prehideSelectors:['div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'],detectCmp:[{exists:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],detectPopup:[{visible:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}],optIn:[{click:'div[class^="CookiesAlert_cookiesAlert__container__"] button'}],optOut:[{hide:'div.aem-page > div[class^="CookiesAlert_cookiesAlert__"]'}]},{name:"transcend",vendorUrl:"https://unknown",cosmetic:!0,prehideSelectors:["#transcend-consent-manager"],detectCmp:[{exists:"#transcend-consent-manager"}],detectPopup:[{visible:"#transcend-consent-manager"}],optIn:[{waitForThenClick:["#transcend-consent-manager","#consentManagerMainDialog .inner-container button"]}],optOut:[{hide:"#transcend-consent-manager"}]},{name:"transip-nl",runContext:{urlPattern:"^https://www\\.transip\\.nl/"},prehideSelectors:["#consent-modal"],detectCmp:[{any:[{exists:"#consent-modal"},{exists:"#privacy-settings-content"}]}],detectPopup:[{any:[{visible:"#consent-modal"},{visible:"#privacy-settings-content"}]}],optIn:[{click:'button[type="submit"]'}],optOut:[{if:{exists:"#privacy-settings-content"},then:[{click:'button[type="submit"]'}],else:[{click:"div.one-modal__action-footer-column--secondary > a"}]}]},{name:"tropicfeel-com",prehideSelectors:["#shopify-section-cookies-controller"],detectCmp:[{exists:"#shopify-section-cookies-controller"}],detectPopup:[{visible:"#shopify-section-cookies-controller #cookies-controller-main-pane",check:"any"}],optIn:[{waitForThenClick:"#cookies-controller-main-pane form[data-form-allow-all] button"}],optOut:[{click:"#cookies-controller-main-pane a[data-tab-target=manage-cookies]"},{waitFor:"#manage-cookies-pane.active"},{click:"#manage-cookies-pane.active input[type=checkbox][checked]:not([disabled])",all:!0},{click:"#manage-cookies-pane.active button[type=submit]"}],test:[]},{name:"true-car",runContext:{urlPattern:"^https://www\\.truecar\\.com/"},cosmetic:!0,prehideSelectors:[['div[aria-labelledby="cookie-banner-heading"]']],detectCmp:[{exists:'div[aria-labelledby="cookie-banner-heading"]'}],detectPopup:[{visible:'div[aria-labelledby="cookie-banner-heading"]'}],optIn:[{click:'div[aria-labelledby="cookie-banner-heading"] > button[aria-label="Close"]'}],optOut:[{hide:'div[aria-labelledby="cookie-banner-heading"]'}]},{name:"truyo",prehideSelectors:["#truyo-consent-module"],detectCmp:[{exists:"#truyo-cookieBarContent"}],detectPopup:[{visible:"#truyo-consent-module"}],optIn:[{click:"button#acceptAllCookieButton"}],optOut:[{click:"button#declineAllCookieButton"}]},{name:"twitch-mobile",vendorUrl:"https://m.twitch.tv/",cosmetic:!0,runContext:{urlPattern:"^https?://m\\.twitch\\.tv"},prehideSelectors:[],detectCmp:[{exists:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],detectPopup:[{visible:'.ReactModal__Overlay [href="https://www.twitch.tv/p/cookie-policy"]'}],optIn:[{waitForThenClick:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"]) button'}],optOut:[{hide:'.ReactModal__Overlay:has([href="https://www.twitch.tv/p/cookie-policy"])'}]},{name:"twitch.tv",runContext:{urlPattern:"^https?://(www\\.)?twitch\\.tv"},prehideSelectors:["div:has(> .consent-banner .consent-banner__content--gdpr-v2),.ReactModalPortal:has([data-a-target=consent-modal-save])"],detectCmp:[{exists:".consent-banner .consent-banner__content--gdpr-v2"}],detectPopup:[{visible:".consent-banner .consent-banner__content--gdpr-v2"}],optIn:[{click:'button[data-a-target="consent-banner-accept"]'}],optOut:[{hide:"div:has(> .consent-banner .consent-banner__content--gdpr-v2)"},{click:'button[data-a-target="consent-banner-manage-preferences"]'},{waitFor:"input[type=checkbox][data-a-target=tw-checkbox]"},{click:"input[type=checkbox][data-a-target=tw-checkbox][checked]:not([disabled])",all:!0,optional:!0},{waitForThenClick:"[data-a-target=consent-modal-save]"},{waitForVisible:".ReactModalPortal:has([data-a-target=consent-modal-save])",check:"none"}]},{name:"twitter",runContext:{urlPattern:"^https://([a-z0-9-]+\\.)?twitter\\.com/"},prehideSelectors:['[data-testid="BottomBar"]'],detectCmp:[{exists:'[data-testid="BottomBar"] div'}],detectPopup:[{visible:'[data-testid="BottomBar"] div'}],optIn:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:first-child'}],optOut:[{waitForThenClick:'[data-testid="BottomBar"] > div:has(>div:first-child>div:last-child>span[role=button]) > div:last-child > div[role=button]:last-child'}],TODOtest:[{eval:"EVAL_document.cookie.includes('d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoy')"}]},{name:"ubuntu.com",prehideSelectors:["dialog.cookie-policy"],detectCmp:[{any:[{exists:"dialog.cookie-policy header"},{exists:'xpath///*[@id="modal"]/div/header'}]}],detectPopup:[{any:[{visible:"dialog header"},{visible:'xpath///*[@id="modal"]/div/header'}]}],optIn:[{any:[{waitForThenClick:"#cookie-policy-button-accept"},{waitForThenClick:'xpath///*[@id="cookie-policy-button-accept"]'}]}],optOut:[{any:[{waitForThenClick:"button.js-manage"},{waitForThenClick:'xpath///*[@id="cookie-policy-content"]/p[4]/button[2]'}]},{waitForThenClick:"dialog.cookie-policy .p-switch__input:checked",optional:!0,all:!0,timeout:500},{any:[{waitForThenClick:"dialog.cookie-policy .js-save-preferences"},{waitForThenClick:'xpath///*[@id="modal"]/div/button'}]}],test:[{eval:"EVAL_UBUNTU_COM_0"}]},{name:"UK Cookie Consent",prehideSelectors:["#catapult-cookie-bar"],cosmetic:!0,detectCmp:[{exists:"#catapult-cookie-bar"}],detectPopup:[{exists:".has-cookie-bar #catapult-cookie-bar"}],optIn:[{click:"#catapultCookie"}],optOut:[{hide:"#catapult-cookie-bar"}],test:[{eval:"EVAL_UK_COOKIE_CONSENT_0"}]},{name:"urbanarmorgear-com",cosmetic:!0,prehideSelectors:['div[class^="Layout__CookieBannerContainer-"]'],detectCmp:[{exists:'div[class^="Layout__CookieBannerContainer-"]'}],detectPopup:[{visible:'div[class^="Layout__CookieBannerContainer-"]'}],optIn:[{click:'button[class^="CookieBanner__AcceptButton"]'}],optOut:[{hide:'div[class^="Layout__CookieBannerContainer-"]'}]},{name:"usercentrics-api",detectCmp:[{exists:"#usercentrics-root"}],detectPopup:[{eval:"EVAL_USERCENTRICS_API_0"},{exists:["#usercentrics-root","[data-testid=uc-container]"]},{waitForVisible:"#usercentrics-root",timeout:2e3}],optIn:[{eval:"EVAL_USERCENTRICS_API_3"},{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_5"}],optOut:[{eval:"EVAL_USERCENTRICS_API_1"},{eval:"EVAL_USERCENTRICS_API_2"}],test:[{eval:"EVAL_USERCENTRICS_API_6"}]},{name:"usercentrics-button",detectCmp:[{exists:"#usercentrics-button"}],detectPopup:[{visible:"#usercentrics-button #uc-btn-accept-banner"}],optIn:[{click:"#usercentrics-button #uc-btn-accept-banner"}],optOut:[{click:"#usercentrics-button #uc-btn-deny-banner"}],test:[{eval:"EVAL_USERCENTRICS_BUTTON_0"}]},{name:"uswitch.com",prehideSelectors:["#cookie-banner-wrapper"],detectCmp:[{exists:"#cookie-banner-wrapper"}],detectPopup:[{visible:"#cookie-banner-wrapper"}],optIn:[{click:"#cookie_banner_accept_mobile"}],optOut:[{click:"#cookie_banner_save"}]},{name:"vodafone.de",runContext:{urlPattern:"^https://www\\.vodafone\\.de/"},prehideSelectors:[".dip-consent,.dip-consent-container"],detectCmp:[{exists:".dip-consent-container"}],detectPopup:[{visible:".dip-consent-content"}],optOut:[{click:'.dip-consent-btn[tabindex="2"]'}],optIn:[{click:'.dip-consent-btn[tabindex="1"]'}]},{name:"waitrose.com",prehideSelectors:["div[aria-labelledby=CookieAlertModalHeading]","section[data-test=initial-waitrose-cookie-consent-banner]","section[data-test=cookie-consent-modal]"],detectCmp:[{exists:"section[data-test=initial-waitrose-cookie-consent-banner]"}],detectPopup:[{visible:"section[data-test=initial-waitrose-cookie-consent-banner]"}],optIn:[{click:"button[data-test=accept-all]"}],optOut:[{click:"button[data-test=manage-cookies]"},{wait:200},{eval:"EVAL_WAITROSE_0"},{click:"button[data-test=submit]"}],test:[{eval:"EVAL_WAITROSE_1"}]},{name:"webflow",vendorUrl:"https://webflow.com/",prehideSelectors:[".fs-cc-components"],detectCmp:[{exists:".fs-cc-components"}],detectPopup:[{visible:".fs-cc-components"},{visible:"[fs-cc=banner]"}],optIn:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=allow]"}],optOut:[{wait:500},{waitForThenClick:"[fs-cc=banner] [fs-cc=deny]"}]},{name:"wetransfer.com",detectCmp:[{exists:".welcome__cookie-notice"}],detectPopup:[{visible:".welcome__cookie-notice"}],optIn:[{click:".welcome__button--accept"}],optOut:[{click:".welcome__button--decline"}]},{name:"whitepages.com",runContext:{urlPattern:"^https://www\\.whitepages\\.com/"},cosmetic:!0,prehideSelectors:[".cookie-wrapper, .cookie-overlay"],detectCmp:[{exists:".cookie-wrapper"}],detectPopup:[{visible:".cookie-overlay"}],optIn:[{click:'button[aria-label="Got it"]'}],optOut:[{hide:".cookie-wrapper"}]},{name:"wolframalpha",vendorUrl:"https://www.wolframalpha.com",prehideSelectors:[],cosmetic:!0,runContext:{urlPattern:"^https://www\\.wolframalpha\\.com/"},detectCmp:[{exists:"section._a_yb"}],detectPopup:[{visible:"section._a_yb"}],optIn:[{waitForThenClick:"section._a_yb button"}],optOut:[{hide:"section._a_yb"}]},{name:"woo-commerce-com",prehideSelectors:[".wccom-comp-privacy-banner .wccom-privacy-banner"],detectCmp:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],detectPopup:[{exists:".wccom-comp-privacy-banner .wccom-privacy-banner"}],optIn:[{click:".wccom-privacy-banner__content-buttons button.is-primary"}],optOut:[{click:".wccom-privacy-banner__content-buttons button.is-secondary"},{waitForThenClick:"input[type=checkbox][checked]:not([disabled])",all:!0},{click:"div.wccom-modal__footer > button"}]},{name:"WP Cookie Notice for GDPR",vendorUrl:"https://wordpress.org/plugins/gdpr-cookie-consent/",prehideSelectors:["#gdpr-cookie-consent-bar"],detectCmp:[{exists:"#gdpr-cookie-consent-bar"}],detectPopup:[{visible:"#gdpr-cookie-consent-bar"}],optIn:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_accept"}],optOut:[{waitForThenClick:"#gdpr-cookie-consent-bar #cookie_action_reject"}],test:[{eval:"EVAL_WP_COOKIE_NOTICE_0"}]},{name:"wpcc",cosmetic:!0,prehideSelectors:[".wpcc-container"],detectCmp:[{exists:".wpcc-container"}],detectPopup:[{exists:".wpcc-container .wpcc-message"}],optIn:[{click:".wpcc-compliance .wpcc-btn"}],optOut:[{hide:".wpcc-container"}]},{name:"xe.com",vendorUrl:"https://www.xe.com/",runContext:{urlPattern:"^https://www\\.xe\\.com/"},prehideSelectors:["[class*=ConsentBanner]"],detectCmp:[{exists:"[class*=ConsentBanner]"}],detectPopup:[{visible:"[class*=ConsentBanner]"}],optIn:[{waitForThenClick:"[class*=ConsentBanner] .egnScw"}],optOut:[{wait:1e3},{waitForThenClick:"[class*=ConsentBanner] .frDWEu"},{waitForThenClick:"[class*=ConsentBanner] .hXIpFU"}],test:[{eval:"EVAL_XE_TEST"}]},{name:"xhamster-eu",prehideSelectors:[".cookies-modal"],detectCmp:[{exists:".cookies-modal"}],detectPopup:[{exists:".cookies-modal"}],optIn:[{click:"button.cmd-button-accept-all"}],optOut:[{click:"button.cmd-button-reject-all"}]},{name:"xhamster-us",runContext:{urlPattern:"^https://(www\\.)?xhamster\\d?\\.com"},cosmetic:!0,prehideSelectors:[".cookie-announce"],detectCmp:[{exists:".cookie-announce"}],detectPopup:[{visible:".cookie-announce .announce-text"}],optIn:[{click:".cookie-announce button.xh-button"}],optOut:[{hide:".cookie-announce"}]},{name:"xing.com",detectCmp:[{exists:"div[class^=cookie-consent-CookieConsent]"}],detectPopup:[{exists:"div[class^=cookie-consent-CookieConsent]"}],optIn:[{click:"#consent-accept-button"}],optOut:[{click:"#consent-settings-button"},{click:".consent-banner-button-accept-overlay"}],test:[{eval:"EVAL_XING_0"}]},{name:"xnxx-com",cosmetic:!0,prehideSelectors:["#cookies-use-alert"],detectCmp:[{exists:"#cookies-use-alert"}],detectPopup:[{visible:"#cookies-use-alert"}],optIn:[{click:"#cookies-use-alert .close"}],optOut:[{hide:"#cookies-use-alert"}]},{name:"xvideos",vendorUrl:"https://xvideos.com",runContext:{urlPattern:"^https://[^/]*xvideos\\.com/"},prehideSelectors:[],detectCmp:[{exists:".disclaimer-opened #disclaimer-cookies"}],detectPopup:[{visible:".disclaimer-opened #disclaimer-cookies"}],optIn:[{waitForThenClick:"#disclaimer-accept_cookies"}],optOut:[{waitForThenClick:"#disclaimer-reject_cookies"}]},{name:"Yahoo",runContext:{urlPattern:"^https://consent\\.yahoo\\.com/v2/"},prehideSelectors:["#reject-all"],detectCmp:[{exists:"#consent-page"}],detectPopup:[{visible:"#consent-page"}],optIn:[{waitForThenClick:"#consent-page button[value=agree]"}],optOut:[{waitForThenClick:"#consent-page button[value=reject]"}]},{name:"youporn.com",cosmetic:!0,prehideSelectors:[".euCookieModal, #js_euCookieModal"],detectCmp:[{exists:".euCookieModal"}],detectPopup:[{exists:".euCookieModal, #js_euCookieModal"}],optIn:[{click:'button[name="user_acceptCookie"]'}],optOut:[{hide:".euCookieModal"}]},{name:"youtube-desktop",prehideSelectors:["tp-yt-iron-overlay-backdrop.opened","ytd-consent-bump-v2-lightbox"],detectCmp:[{exists:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"},{exists:'ytd-consent-bump-v2-lightbox tp-yt-paper-dialog a[href^="https://consent.youtube.com/"]'}],detectPopup:[{visible:"ytd-consent-bump-v2-lightbox tp-yt-paper-dialog"}],optIn:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:last-child button"},{wait:500}],optOut:[{waitForThenClick:"ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child #button,ytd-consent-bump-v2-lightbox .eom-buttons .eom-button-row:first-child ytd-button-renderer:first-child button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_DESKTOP_0"}]},{name:"youtube-mobile",prehideSelectors:[".consent-bump-v2-lightbox"],detectCmp:[{exists:"ytm-consent-bump-v2-renderer"}],detectPopup:[{visible:"ytm-consent-bump-v2-renderer"}],optIn:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:first-child button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:first-child button"},{wait:500}],optOut:[{waitForThenClick:"ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons c3-material-button:nth-child(2) button, ytm-consent-bump-v2-renderer .privacy-terms + .one-col-dialog-buttons ytm-button-renderer:nth-child(2) button"},{wait:500}],test:[{wait:500},{eval:"EVAL_YOUTUBE_MOBILE_0"}]},{name:"zdf",prehideSelectors:["#zdf-cmp-banner-sdk"],detectCmp:[{exists:"#zdf-cmp-banner-sdk"}],detectPopup:[{visible:"#zdf-cmp-main.zdf-cmp-show"}],optIn:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-accept-btn"}],optOut:[{waitForThenClick:"#zdf-cmp-main #zdf-cmp-deny-btn"}],test:[]}],C={"didomi.io":{detectors:[{presentMatcher:{target:{selector:"#didomi-host, #didomi-notice"},type:"css"},showingMatcher:{target:{selector:"body.didomi-popup-open, .didomi-notice-banner"},type:"css"}}],methods:[{action:{target:{selector:".didomi-popup-notice-buttons .didomi-button:not(.didomi-button-highlight), .didomi-notice-banner .didomi-learn-more-button"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{retries:50,target:{selector:"#didomi-purpose-cookies"},type:"waitcss",waitTime:50},{consents:[{description:"Share (everything) with others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-share_whith_others]:last-child"},type:"click"},type:"X"},{description:"Information storage and access",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-cookies]:last-child"},type:"click"},type:"D"},{description:"Content selection, offers and marketing",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-CL-T1Rgm7]:last-child"},type:"click"},type:"E"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-analytics]:last-child"},type:"click"},type:"B"},{description:"Analytics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-M9NRHJe3G]:last-child"},type:"click"},type:"B"},{description:"Ad and content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-advertising_personalization]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection",falseAction:{parent:{childFilter:{target:{selector:"#didomi-purpose-pub-ciblee"}},selector:".didomi-consent-popup-data-processing, .didomi-components-accordion-label-container"},target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-pub-ciblee]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - basics",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-q4zlJqdcD]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - partners and subsidiaries",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-partenaire-cAsDe8jC]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-p4em9a8m]:last-child"},type:"click"},type:"F"},{description:"Ad and content selection - others",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-autres-pub]:last-child"},type:"click"},type:"F"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-reseauxsociaux]:last-child"},type:"click"},type:"A"},{description:"Social networks",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-social_media]:last-child"},type:"click"},type:"A"},{description:"Content selection",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-content_personalization]:last-child"},type:"click"},type:"E"},{description:"Ad delivery",falseAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:first-child"},type:"click"},trueAction:{target:{selector:".didomi-components-radio__option[aria-describedby=didomi-purpose-ad_delivery]:last-child"},type:"click"},type:"F"}],type:"consent"},{action:{consents:[{matcher:{childFilter:{target:{selector:":not(.didomi-components-radio__option--selected)"}},type:"css"},trueAction:{target:{selector:":nth-child(2)"},type:"click"},falseAction:{target:{selector:":first-child"},type:"click"},type:"X"}],type:"consent"},target:{selector:".didomi-components-radio"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".didomi-consent-popup-footer .didomi-consent-popup-actions"},target:{selector:".didomi-components-button:first-child"},type:"click"},name:"SAVE_CONSENT"}]},oil:{detectors:[{presentMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"},showingMatcher:{target:{selector:".as-oil-content-overlay"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".as-js-advanced-settings"},type:"click"},{retries:"10",target:{selector:".as-oil-cpc__purpose-container"},type:"waitcss",waitTime:"250"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{consents:[{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Information storage and access","Opbevaring af og adgang til oplysninger på din enhed"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"D"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personlige annoncer","Personalisation"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Annoncevalg, levering og rapportering","Ad selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:["Personalisering af indhold","Content selection, delivery, reporting"]},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"E"},{matcher:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{childFilter:{target:{selector:".as-oil-cpc__purpose-header",textFilter:["Måling","Measurement"]}},selector:".as-oil-cpc__purpose-container"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"B"},{matcher:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".as-oil-cpc__purpose-container",textFilter:"Google"},target:{selector:".as-oil-cpc__switch"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:".as-oil__btn-optin"},type:"click"},name:"SAVE_CONSENT"},{action:{target:{selector:"div.as-oil"},type:"hide"},name:"HIDE_CMP"}]},optanon:{detectors:[{presentMatcher:{target:{selector:"#optanon-menu, .optanon-alert-box-wrapper"},type:"css"},showingMatcher:{target:{displayFilter:!0,selector:".optanon-alert-box-wrapper"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".optanon-alert-box-wrapper .optanon-toggle-display, a[onclick*='OneTrust.ToggleInfoDisplay()'], a[onclick*='Optanon.ToggleInfoDisplay()']"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".preference-menu-item #Your-privacy"},type:"click"},{target:{selector:"#optanon-vendor-consent-text"},type:"click"},{action:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},target:{selector:"#optanon-vendor-consent-list .vendor-item"},type:"foreach"},{target:{selector:".vendor-consent-back-link"},type:"click"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-performance"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-functional"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-advertising"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-social"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Social Media Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalisation"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Site monitoring cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Third party privacy-enhanced content"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Performance & Advertising Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Information storage and access"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"D"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content selection, delivery, reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Measurement"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Recommended Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Unclassified Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"X"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Analytical Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"B"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Marketing Cookies"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Personalization"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Ad Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},type:"ifcss"},{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},trueAction:{actions:[{parent:{selector:"#optanon-menu, .optanon-menu"},target:{selector:".menu-item-necessary",textFilter:"Content Selection, Delivery & Reporting"},type:"click"},{consents:[{matcher:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status input"},type:"checkbox"},toggleAction:{parent:{selector:"#optanon-popup-body-right"},target:{selector:".optanon-status label"},type:"click"},type:"E"}],type:"consent"}],type:"list"},type:"ifcss"}],type:"list"},name:"DO_CONSENT"},{action:{parent:{selector:".optanon-save-settings-button"},target:{selector:".optanon-white-button-middle"},type:"click"},name:"SAVE_CONSENT"},{action:{actions:[{target:{selector:"#optanon-popup-wrapper"},type:"hide"},{target:{selector:"#optanon-popup-bg"},type:"hide"},{target:{selector:".optanon-alert-box-wrapper"},type:"hide"}],type:"list"},name:"HIDE_CMP"}]},quantcast2:{detectors:[{presentMatcher:{target:{selector:"[data-tracking-opt-in-overlay]"},type:"css"},showingMatcher:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"css"}}],methods:[{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-learn-more]"},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{type:"wait",waitTime:500},{action:{actions:[{target:{selector:"div",textFilter:["Information storage and access"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"D"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Personalization"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Ad selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"F"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Content selection, delivery, reporting"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"E"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Measurement"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"B"}],type:"consent"},type:"ifcss"},{target:{selector:"div",textFilter:["Other Partners"]},trueAction:{consents:[{matcher:{target:{selector:"input"},type:"checkbox"},toggleAction:{target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},type:"ifcss"}],type:"list"},parent:{childFilter:{target:{selector:"input"}},selector:"[data-tracking-opt-in-overlay] > div > div"},target:{childFilter:{target:{selector:"input"}},selector:":scope > div"},type:"foreach"}],type:"list"},name:"DO_CONSENT"},{action:{target:{selector:"[data-tracking-opt-in-overlay] [data-tracking-opt-in-save]"},type:"click"},name:"SAVE_CONSENT"}]},springer:{detectors:[{presentMatcher:{parent:null,target:{selector:".cmp-app_gdpr"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".cmp-popup_popup"},type:"css"}}],methods:[{action:{actions:[{target:{selector:".cmp-intro_rejectAll"},type:"click"},{type:"wait",waitTime:250},{target:{selector:".cmp-purposes_purposeItem:not(.cmp-purposes_selectedPurpose)"},type:"click"}],type:"list"},name:"OPEN_OPTIONS"},{action:{consents:[{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Przechowywanie informacji na urządzeniu lub dostęp do nich",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"D"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór podstawowych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"F"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Tworzenie profilu spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"E"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Wybór spersonalizowanych treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności reklam",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Pomiar wydajności treści",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"B"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Stosowanie badań rynkowych w celu generowania opinii odbiorców",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"},{matcher:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch .cmp-switch_isSelected"},type:"css"},toggleAction:{parent:{selector:".cmp-purposes_detailHeader",textFilter:"Opracowywanie i ulepszanie produktów",childFilter:{target:{selector:".cmp-switch_switch"}}},target:{selector:".cmp-switch_switch:not(.cmp-switch_isSelected)"},type:"click"},type:"X"}],type:"consent"},name:"DO_CONSENT"},{action:{target:{selector:".cmp-details_save"},type:"click"},name:"SAVE_CONSENT"}]},wordpressgdpr:{detectors:[{presentMatcher:{parent:null,target:{selector:".wpgdprc-consent-bar"},type:"css"},showingMatcher:{parent:null,target:{displayFilter:!0,selector:".wpgdprc-consent-bar"},type:"css"}}],methods:[{action:{parent:null,target:{selector:".wpgdprc-consent-bar .wpgdprc-consent-bar__settings",textFilter:null},type:"click"},name:"OPEN_OPTIONS"},{action:{actions:[{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Eyeota"},type:"click"},{consents:[{description:"Eyeota Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Eyeota"},target:{selector:"label"},type:"click"},type:"X"}],type:"consent"},{target:{selector:".wpgdprc-consent-modal .wpgdprc-button",textFilter:"Advertising"},type:"click"},{consents:[{description:"Advertising Cookies",matcher:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"input"},type:"checkbox"},toggleAction:{parent:{selector:".wpgdprc-consent-modal__description",textFilter:"Advertising"},target:{selector:"label"},type:"click"},type:"F"}],type:"consent"}],type:"list"},name:"DO_CONSENT"},{action:{parent:null,target:{selector:".wpgdprc-button",textFilter:"Save my settings"},type:"click"},name:"SAVE_CONSENT"}]}},v={autoconsent:w,consentomatic:C},f=Object.freeze({__proto__:null,autoconsent:w,consentomatic:C,default:v});const A=new class{constructor(e,t=null,o=null){if(this.id=n(),this.rules=[],this.foundCmp=null,this.state={lifecycle:"loading",prehideOn:!1,findCmpAttempts:0,detectedCmps:[],detectedPopups:[],selfTest:null},a.sendContentMessage=e,this.sendContentMessage=e,this.rules=[],this.updateState({lifecycle:"loading"}),this.addDynamicRules(),t)this.initialize(t,o);else{o&&this.parseDeclarativeRules(o);e({type:"init",url:window.location.href}),this.updateState({lifecycle:"waitingForInitResponse"})}this.domActions=new class{constructor(e){this.autoconsentInstance=e}click(e,t=!1){const o=this.elementSelector(e);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[click]",e,t,o),o.length>0&&(t?o.forEach((e=>e.click())):o[0].click()),o.length>0}elementExists(e){return this.elementSelector(e).length>0}elementVisible(e,t){const o=this.elementSelector(e),c=new Array(o.length);return o.forEach(((e,t)=>{c[t]=k(e)})),"none"===t?c.every((e=>!e)):0!==c.length&&("any"===t?c.some((e=>e)):c.every((e=>e)))}waitForElement(e,t=1e4){const o=Math.ceil(t/200);return this.autoconsentInstance.config.logs.rulesteps&&console.log("[waitForElement]",e),h((()=>this.elementSelector(e).length>0),o,200)}waitForVisible(e,t=1e4,o="any"){return h((()=>this.elementVisible(e,o)),Math.ceil(t/200),200)}async waitForThenClick(e,t=1e4,o=!1){return await this.waitForElement(e,t),this.click(e,o)}wait(e){return new Promise((t=>{setTimeout((()=>{t(!0)}),e)}))}hide(e,t){return m(u(),e,t)}prehide(e){const t=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[prehide]",t,location.href),m(t,e,"opacity")}undoPrehide(){const e=u("autoconsent-prehide");return this.autoconsentInstance.config.logs.lifecycle&&console.log("[undoprehide]",e,location.href),e&&e.remove(),!!e}querySingleReplySelector(e,t=document){if(e.startsWith("aria/"))return[];if(e.startsWith("xpath/")){const o=e.slice(6),c=document.evaluate(o,t,null,XPathResult.ANY_TYPE,null);let i=null;const n=[];for(;i=c.iterateNext();)n.push(i);return n}return e.startsWith("text/")||e.startsWith("pierce/")?[]:t.shadowRoot?Array.from(t.shadowRoot.querySelectorAll(e)):Array.from(t.querySelectorAll(e))}querySelectorChain(e){let t,o=document;for(const c of e){if(t=this.querySingleReplySelector(c,o),0===t.length)return[];o=t[0]}return t}elementSelector(e){return"string"==typeof e?this.querySingleReplySelector(e):this.querySelectorChain(e)}}(this)}initialize(e,t){const o=b(e);if(o.logs.lifecycle&&console.log("autoconsent init",window.location.href),this.config=o,o.enabled){if(t&&this.parseDeclarativeRules(t),this.rules=function(e,t){return e.filter((e=>(!t.disabledCmps||!t.disabledCmps.includes(e.name))&&(t.enableCosmeticRules||!e.isCosmetic)))}(this.rules,o),e.enablePrehide)if(document.documentElement)this.prehideElements();else{const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.prehideElements()};window.addEventListener("DOMContentLoaded",e)}if("loading"===document.readyState){const e=()=>{window.removeEventListener("DOMContentLoaded",e),this.start()};window.addEventListener("DOMContentLoaded",e)}else this.start();this.updateState({lifecycle:"initialized"})}else o.logs.lifecycle&&console.log("autoconsent is disabled")}addDynamicRules(){y.forEach((e=>{this.rules.push(new e(this))}))}parseDeclarativeRules(e){Object.keys(e.consentomatic).forEach((t=>{this.addConsentomaticCMP(t,e.consentomatic[t])})),e.autoconsent.forEach((e=>{this.addDeclarativeCMP(e)}))}addDeclarativeCMP(e){this.rules.push(new d(e,this))}addConsentomaticCMP(e,t){this.rules.push(new class{constructor(e,t){this.name=e,this.config=t,this.methods=new Map,this.runContext=l,this.isCosmetic=!1,t.methods.forEach((e=>{e.action&&this.methods.set(e.name,e.action)})),this.hasSelfTest=!1}get isIntermediate(){return!1}checkRunContext(){return!0}async detectCmp(){return this.config.detectors.map((e=>o(e.presentMatcher))).some((e=>!!e))}async detectPopup(){return this.config.detectors.map((e=>o(e.showingMatcher))).some((e=>!!e))}async executeAction(e,t){return!this.methods.has(e)||c(this.methods.get(e),t)}async optOut(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",[]),await this.executeAction("SAVE_CONSENT"),!0}async optIn(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),await this.executeAction("HIDE_CMP"),await this.executeAction("DO_CONSENT",["D","A","B","E","F","X"]),await this.executeAction("SAVE_CONSENT"),!0}async openCmp(){return await this.executeAction("HIDE_CMP"),await this.executeAction("OPEN_OPTIONS"),!0}async test(){return!0}}(`com_${e}`,t))}start(){window.requestIdleCallback?window.requestIdleCallback((()=>this._start()),{timeout:500}):this._start()}async _start(){const e=this.config.logs;e.lifecycle&&console.log(`Detecting CMPs on ${window.location.href}`),this.updateState({lifecycle:"started"});const t=await this.findCmp(this.config.detectRetries);if(this.updateState({detectedCmps:t.map((e=>e.name))}),0===t.length)return e.lifecycle&&console.log("no CMP found",location.href),this.config.enablePrehide&&this.undoPrehide(),this.updateState({lifecycle:"nothingDetected"}),!1;this.updateState({lifecycle:"cmpDetected"});const o=[],c=[];for(const e of t)e.isCosmetic?c.push(e):o.push(e);let i=!1,n=await this.detectPopups(o,(async e=>{i=await this.handlePopup(e)}));if(0===n.length&&(n=await this.detectPopups(c,(async e=>{i=await this.handlePopup(e)}))),0===n.length)return e.lifecycle&&console.log("no popup found"),this.config.enablePrehide&&this.undoPrehide(),!1;if(n.length>1){const t={msg:"Found multiple CMPs, check the detection rules.",cmps:n.map((e=>e.name))};e.errors&&console.warn(t.msg,t.cmps),this.sendContentMessage({type:"autoconsentError",details:t})}return i}async findCmp(e){const t=this.config.logs;this.updateState({findCmpAttempts:this.state.findCmpAttempts+1});const o=[];for(const e of this.rules)try{if(!e.checkRunContext())continue;await e.detectCmp()&&(t.lifecycle&&console.log(`Found CMP: ${e.name} ${window.location.href}`),this.sendContentMessage({type:"cmpDetected",url:location.href,cmp:e.name}),o.push(e))}catch(o){t.errors&&console.warn(`error detecting ${e.name}`,o)}return 0===o.length&&e>0?(await this.domActions.wait(500),this.findCmp(e-1)):o}async detectPopup(e){if(await this.waitForPopup(e).catch((t=>(this.config.logs.errors&&console.warn(`error waiting for a popup for ${e.name}`,t),!1))))return this.updateState({detectedPopups:this.state.detectedPopups.concat([e.name])}),this.sendContentMessage({type:"popupFound",cmp:e.name,url:location.href}),e;throw new Error("Popup is not shown")}async detectPopups(e,t){const o=e.map((e=>this.detectPopup(e)));await Promise.any(o).then((e=>{t(e)})).catch((()=>null));const c=await Promise.allSettled(o),i=[];for(const e of c)"fulfilled"===e.status&&i.push(e.value);return i}async handlePopup(e){return this.updateState({lifecycle:"openPopupDetected"}),this.config.enablePrehide&&!this.state.prehideOn&&this.prehideElements(),this.foundCmp=e,"optOut"===this.config.autoAction?await this.doOptOut():"optIn"===this.config.autoAction?await this.doOptIn():(this.config.logs.lifecycle&&console.log("waiting for opt-out signal...",location.href),!0)}async doOptOut(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptOut"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt out on ${window.location.href}`),t=await this.foundCmp.optOut(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt out result ${t}`)):(e.errors&&console.log("no CMP to opt out"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optOutResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:this.foundCmp&&this.foundCmp.hasSelfTest,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optOutSucceeded":"optOutFailed"}),t}async doOptIn(){const e=this.config.logs;let t;return this.updateState({lifecycle:"runningOptIn"}),this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: opt in on ${window.location.href}`),t=await this.foundCmp.optIn(),e.lifecycle&&console.log(`${this.foundCmp.name}: opt in result ${t}`)):(e.errors&&console.log("no CMP to opt in"),t=!1),this.config.enablePrehide&&this.undoPrehide(),this.sendContentMessage({type:"optInResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,scheduleSelfTest:!1,url:location.href}),t&&!this.foundCmp.isIntermediate?(this.sendContentMessage({type:"autoconsentDone",cmp:this.foundCmp.name,isCosmetic:this.foundCmp.isCosmetic,url:location.href}),this.updateState({lifecycle:"done"})):this.updateState({lifecycle:t?"optInSucceeded":"optInFailed"}),t}async doSelfTest(){const e=this.config.logs;let t;return this.foundCmp?(e.lifecycle&&console.log(`CMP ${this.foundCmp.name}: self-test on ${window.location.href}`),t=await this.foundCmp.test()):(e.errors&&console.log("no CMP to self test"),t=!1),this.sendContentMessage({type:"selfTestResult",cmp:this.foundCmp?this.foundCmp.name:"none",result:t,url:location.href}),this.updateState({selfTest:t}),t}async waitForPopup(e,t=5,o=500){const c=this.config.logs;c.lifecycle&&console.log("checking if popup is open...",e.name);const i=await e.detectPopup().catch((t=>(c.errors&&console.warn(`error detecting popup for ${e.name}`,t),!1)));return!i&&t>0?(await this.domActions.wait(o),this.waitForPopup(e,t-1,o)):(c.lifecycle&&console.log(e.name,"popup is "+(i?"open":"not open")),i)}prehideElements(){const e=this.config.logs,t=this.rules.filter((e=>e.prehideSelectors&&e.checkRunContext())).reduce(((e,t)=>[...e,...t.prehideSelectors]),["#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium"]);return this.updateState({prehideOn:!0}),setTimeout((()=>{this.config.enablePrehide&&this.state.prehideOn&&!["runningOptOut","runningOptIn"].includes(this.state.lifecycle)&&(e.lifecycle&&console.log("Process is taking too long, unhiding elements"),this.undoPrehide())}),this.config.prehideTimeout||2e3),this.domActions.prehide(t.join(","))}undoPrehide(){return this.updateState({prehideOn:!1}),this.domActions.undoPrehide()}updateState(e){Object.assign(this.state,e),this.sendContentMessage({type:"report",instanceId:this.id,url:window.location.href,mainFrame:window.top===window.self,state:this.state})}async receiveMessageCallback(e){const t=this.config?.logs;switch(t?.messages&&console.log("received from background",e,window.location.href),e.type){case"initResp":this.initialize(e.config,e.rules);break;case"optIn":await this.doOptIn();break;case"optOut":await this.doOptOut();break;case"selfTest":await this.doSelfTest();break;case"evalResp":!function(e,t){const o=a.pending.get(e);o?(a.pending.delete(e),o.timer&&window.clearTimeout(o.timer),o.resolve(t)):console.warn("no eval #",e)}(e.id,e.result)}}}((e=>{window.webkit.messageHandlers[e.type]&&window.webkit.messageHandlers[e.type].postMessage(e).then((e=>{A.receiveMessageCallback(e)}))}),null,f);window.autoconsentMessageCallback=e=>{A.receiveMessageCallback(e)}}(); diff --git a/package-lock.json b/package-lock.json index a506b40d5d..b3d356236a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "macos-browser", "version": "1.0.0", "dependencies": { - "@duckduckgo/autoconsent": "^10.3.0" + "@duckduckgo/autoconsent": "^10.5.0" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -53,9 +53,9 @@ } }, "node_modules/@duckduckgo/autoconsent": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.3.0.tgz", - "integrity": "sha512-dUf37qkaYDuXEytU9mNNLGw28S1t1M1dFnvMHZDV9BpINVJeAl1ye7CmlABuGlDs6URrp2ZLZ5IxcKQhQglYcw==" + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@duckduckgo/autoconsent/-/autoconsent-10.5.0.tgz", + "integrity": "sha512-4mdp9mwBiE+IKTvN84iRA8d7eSkJ5xMaQvhvbgw7XlD1VOJlfiJPhP8PJWV+wyc7DNVHMtcdUXiD+ICw/SJBRA==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", diff --git a/package.json b/package.json index ee09d66203..dbae81bb89 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "rollup-plugin-terser": "^7.0.2" }, "dependencies": { - "@duckduckgo/autoconsent": "^10.3.0" + "@duckduckgo/autoconsent": "^10.5.0" } } From 8989cfaec9f5b2b79c21c90dc1e82225ec0480b1 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 9 Apr 2024 14:35:00 +0500 Subject: [PATCH 036/221] keep Downloads button when opened using Cmd+J (#2577) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206968292746694/f --- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 171e6699c1..4f2befbe8b 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -392,7 +392,7 @@ extension MainViewController { } navigationBarViewController.view.window?.makeKeyAndOrderFront(nil) } - navigationBarViewController.toggleDownloadsPopover(keepButtonVisible: false) + navigationBarViewController.toggleDownloadsPopover(keepButtonVisible: sender is NSMenuItem /* keep button visible for some time on Cmd+J */) } @objc func toggleBookmarksBarFromMenu(_ sender: Any) { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index edd999e760..950cdb4056 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -1028,7 +1028,7 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } func optionsButtonMenuRequestedDownloadsPopover(_ menu: NSMenu) { - toggleDownloadsPopover(keepButtonVisible: false) + toggleDownloadsPopover(keepButtonVisible: true) } func optionsButtonMenuRequestedPrint(_ menu: NSMenu) { From dff594e4f16f62cb5d2c964b68fbda20a7abc464 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 9 Apr 2024 14:50:07 +0500 Subject: [PATCH 037/221] Fix download popup appearing on launch (#2578) Task/Issue URL: https://app.asana.com/0/0/1207025976231577/f --- .../Services/DownloadListCoordinator.swift | 13 ++++---- .../View/NavigationBarViewController.swift | 24 +++++++------- .../DownloadListCoordinatorTests.swift | 33 ++++++++++++------- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift index a7d0b3e1d3..5e9c9e14c9 100644 --- a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift +++ b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift @@ -54,7 +54,7 @@ final class DownloadListCoordinator { enum UpdateKind { case added case removed - case updated + case updated(oldValue: DownloadListItem) } typealias Update = (kind: UpdateKind, item: DownloadListItem) private let updatesSubject = PassthroughSubject() @@ -379,8 +379,8 @@ final class DownloadListCoordinator { case (.none, .some(let item)): self.updatesSubject.send((.added, item)) store.save(item) - case (.some, .some(let item)): - self.updatesSubject.send((.updated, item)) + case (.some(let oldValue), .some(let item)): + self.updatesSubject.send((.updated(oldValue: oldValue), item)) store.save(item) case (.some(let item), .none): item.progress?.cancel() @@ -446,10 +446,9 @@ final class DownloadListCoordinator { @MainActor func downloads(sortedBy keyPath: KeyPath, ascending: Bool) -> [DownloadListItem] { - let comparator: (T, T) -> Bool = ascending ? (<) : (>) - return items.values.sorted(by: { - comparator($0[keyPath: keyPath], $1[keyPath: keyPath]) - }) + return items.values.sorted { + ascending ? ($0[keyPath: keyPath] < $1[keyPath: keyPath]) : ($0[keyPath: keyPath] > $1[keyPath: keyPath]) + } } var updates: AnyPublisher { diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 950cdb4056..28f197478c 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -636,22 +636,20 @@ final class NavigationBarViewController: NSViewController { .sink { [weak self] update in guard let self else { return } - let shouldShowPopover = update.kind == .updated - && DownloadsPreferences.shared.shouldOpenPopupOnCompletion - && update.item.destinationURL != nil - && update.item.tempURL == nil - && !update.item.isBurner - && WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window - - if shouldShowPopover { + if case .updated(let oldValue) = update.kind, + DownloadsPreferences.shared.shouldOpenPopupOnCompletion, + update.item.destinationURL != nil, + update.item.tempURL == nil, + oldValue.tempURL != nil, // download finished + !update.item.isBurner, + WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window { + self.popovers.showDownloadsPopoverAndAutoHide(usingView: downloadsButton, popoverDelegate: self, downloadsDelegate: self) - } else { - if update.item.isBurner { - invalidateDownloadButtonHidingTimer() - updateDownloadsButton(updatingFromPinnedViewsNotification: false) - } + } else if update.item.isBurner { + invalidateDownloadButtonHidingTimer() + updateDownloadsButton(updatingFromPinnedViewsNotification: false) } updateDownloadsButton() } diff --git a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift index 1092aac187..8446b932b2 100644 --- a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift +++ b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift @@ -75,7 +75,7 @@ final class DownloadListCoordinatorTests: XCTestCase { let e = expectation(description: "download added") var id: UUID! let c = coordinator.updates.sink { kind, item in - if kind == .added { + if case .added = kind { e.fulfill() id = item.identifier } @@ -153,9 +153,10 @@ final class DownloadListCoordinatorTests: XCTestCase { } let c = coordinator.updates.sink { (kind, item) in - if kind == .added { + if case .added = kind { expectations[item.identifier]!.fulfill() - } else if kind != .updated { + } else if case .updated = kind { + } else { XCTFail("unexpected \(kind) \(item.fileName)") } } @@ -217,7 +218,7 @@ final class DownloadListCoordinatorTests: XCTestCase { let taskCompleted = expectation(description: "item updated") var c: AnyCancellable! c = coordinator.updates.sink { (kind, item) in - guard kind == .updated, item.progress == nil else { return } + guard case .updated = kind, item.progress == nil else { return } taskCompleted.fulfill() @@ -267,7 +268,9 @@ final class DownloadListCoordinatorTests: XCTestCase { let taskCompleted = expectation(description: "location updated") var c: AnyCancellable! c = coordinator.updates.sink { (kind, item) in - XCTAssertEqual(kind, .updated) + if case .updated = kind { } else { + XCTFail("\(kind) is not .updated") + } guard item.destinationURL != nil, item.tempURL != nil else { return } XCTAssertEqual(item.destinationURL, self.destURL) @@ -328,7 +331,9 @@ final class DownloadListCoordinatorTests: XCTestCase { let itemUpdated = expectation(description: "item updated") let c = coordinator.updates.sink { (kind, item) in - XCTAssertEqual(kind, .updated) + if case .updated = kind { } else { + XCTFail("\(kind) is not .updated") + } itemUpdated.fulfill() XCTAssertEqual(item.destinationURL, item.destinationURL) @@ -386,7 +391,9 @@ final class DownloadListCoordinatorTests: XCTestCase { let itemUpdated = expectation(description: "item updated") let c = coordinator.updates.sink { (kind, item) in - XCTAssertEqual(kind, .updated) + if case .updated = kind { } else { + XCTFail("\(kind) is not .updated") + } itemUpdated.fulfill() XCTAssertEqual(item.destinationURL, item.destinationURL) @@ -444,7 +451,9 @@ final class DownloadListCoordinatorTests: XCTestCase { let itemUpdated = expectation(description: "item updated") let c = coordinator.updates.sink { (kind, item) in - XCTAssertEqual(kind, .updated) + if case .updated = kind { } else { + XCTFail("\(kind) is not .updated") + } itemUpdated.fulfill() XCTAssertEqual(item.destinationURL, item.destinationURL) @@ -467,7 +476,9 @@ final class DownloadListCoordinatorTests: XCTestCase { let itemRemoved = expectation(description: "item removed") let c = coordinator.updates.sink { (kind, item) in - XCTAssertEqual(kind, .removed) + if case .removed = kind { } else { + XCTFail("\(kind) is not .updated") + } itemRemoved.fulfill() XCTAssertEqual(item.identifier, id) @@ -499,7 +510,7 @@ final class DownloadListCoordinatorTests: XCTestCase { let e1 = expectation(description: "download stopped") e1.expectedFulfillmentCount = 2 var c = coordinator.updates.sink { (kind, item) in - guard kind == .updated, item.progress == nil else { return } + guard case .updated = kind, item.progress == nil else { return } e1.fulfill() XCTAssertNotEqual(item.identifier, keptId) } @@ -509,7 +520,7 @@ final class DownloadListCoordinatorTests: XCTestCase { let e2 = expectation(description: "item removed") e2.expectedFulfillmentCount = 2 c = coordinator.updates.sink { (kind, item) in - guard kind == .removed else { return } + guard case .removed = kind else { return } e2.fulfill() XCTAssertNotEqual(item.identifier, keptId) } From a0819ab2784775240732e0dbda2238074087ab01 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 9 Apr 2024 12:30:21 +0200 Subject: [PATCH 038/221] Fixes some IPC issues that were causing high CPU usage (#2582) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207034395541856/f ## Description Fixes some issues with IPC causing CPU usage to spike up considerably. --- .../NetworkProtectionNavBarButtonModel.swift | 8 ++++--- .../Model/PreferencesSidebarModel.swift | 13 ++++++++---- .../View/PrivacyDashboardViewController.swift | 2 +- DuckDuckGo/Tab/Model/Tab.swift | 21 ++++++------------- .../NetworkProtectionFeatureVisibility.swift | 2 ++ UnitTests/Menus/MoreOptionsMenuTests.swift | 8 +++++-- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index d8d1aaedf2..ee8b4d550b 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -36,8 +36,9 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { private var cancellables = Set() - // MARK: - NetP Icon publisher + // MARK: - VPN + private let vpnVisibility: NetworkProtectionFeatureVisibility private let iconPublisher: NetworkProtectionIconPublisher private var iconPublisherCancellable: AnyCancellable? @@ -70,10 +71,12 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { init(popoverManager: NetPPopoverManager, pinningManager: PinningManager = LocalPinningManager.shared, + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), statusReporter: NetworkProtectionStatusReporter, iconProvider: IconProvider = NavigationBarIconProvider()) { self.popoverManager = popoverManager + self.vpnVisibility = vpnVisibility self.networkProtectionStatusReporter = statusReporter self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: networkProtectionStatusReporter, iconProvider: iconProvider) self.pinningManager = pinningManager @@ -175,8 +178,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { @MainActor func updateVisibility() { // The button is visible in the case where NetP has not been activated, but the user has been invited and they haven't accepted T&Cs. - let networkProtectionVisibility = DefaultNetworkProtectionVisibility() - if networkProtectionVisibility.isNetworkProtectionBetaVisible() { + if vpnVisibility.isNetworkProtectionBetaVisible() { if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { showButton = true return diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 3cb7adc0a7..98de79c2a5 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -32,6 +32,7 @@ final class PreferencesSidebarModel: ObservableObject { @Published private(set) var sections: [PreferencesSection] = [] @Published var selectedTabIndex: Int = 0 @Published private(set) var selectedPane: PreferencePaneIdentifier = .defaultBrowser + private let vpnVisibility: NetworkProtectionFeatureVisibility var selectedTabContent: AnyPublisher { $selectedTabIndex.map { [tabSwitcherTabs] in tabSwitcherTabs[$0] }.eraseToAnyPublisher() @@ -43,10 +44,12 @@ final class PreferencesSidebarModel: ObservableObject { loadSections: @escaping () -> [PreferencesSection], tabSwitcherTabs: [Tab.TabContent], privacyConfigurationManager: PrivacyConfigurationManaging, - syncService: DDGSyncing + syncService: DDGSyncing, + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility() ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs + self.vpnVisibility = vpnVisibility resetTabSelectionIfNeeded() refreshSections() @@ -77,11 +80,12 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), includeDuckPlayer: Bool, userDefaults: UserDefaults = .netP ) { let loadSections = { - let includingVPN = DefaultNetworkProtectionVisibility().isInstalled + let includingVPN = vpnVisibility.isInstalled return PreferencesSection.defaultSections( includingDuckPlayer: includeDuckPlayer, @@ -93,13 +97,14 @@ final class PreferencesSidebarModel: ObservableObject { self.init(loadSections: loadSections, tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: privacyConfigurationManager, - syncService: syncService) + syncService: syncService, + vpnVisibility: vpnVisibility) } // MARK: - Setup private func setupVPNPaneVisibility() { - DefaultNetworkProtectionVisibility().onboardStatusPublisher + vpnVisibility.onboardStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index 4a5d5113f3..dd06a09fb7 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -367,7 +367,7 @@ extension PrivacyDashboardViewController { errors: errors, httpStatusCodes: statusCodes, openerContext: currentTab.inferredOpenerContext, - vpnOn: currentTab.tunnelController?.isConnected ?? false, + vpnOn: currentTab.tunnelController.isConnected, jsPerformance: webVitals, userRefreshCount: currentTab.refreshCountSinceLoad, didOpenReportInfo: didOpenReportInfo, diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 643da5aa94..bfa43c24e3 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -341,7 +341,7 @@ protocol NewWindowPolicyDecisionMaker { private let internalUserDecider: InternalUserDecider? let pinnedTabsManager: PinnedTabsManager - private(set) var tunnelController: NetworkProtectionIPCTunnelController? + private(set) var tunnelController: NetworkProtectionIPCTunnelController private let webViewConfiguration: WKWebViewConfiguration @@ -510,6 +510,10 @@ protocol NewWindowPolicyDecisionMaker { duckPlayer: duckPlayer, downloadManager: downloadManager)) + let ipcClient = TunnelControllerIPCClient() + ipcClient.register() + tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) + super.init() tabGetter = { [weak self] in self } userContentController.map(userContentControllerPromise.fulfill) @@ -529,17 +533,6 @@ protocol NewWindowPolicyDecisionMaker { self?.onDuckDuckGoEmailSignOut(notification) } - netPOnboardStatusCancellabel = DefaultNetworkProtectionVisibility().onboardStatusPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] onboardingStatus in - guard onboardingStatus == .completed else { return } - - let ipcClient = TunnelControllerIPCClient() - ipcClient.register() - - self?.tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) - } - self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -1170,8 +1163,6 @@ protocol NewWindowPolicyDecisionMaker { private var webViewCancellables = Set() private var emailDidSignOutCancellable: AnyCancellable? - private var netPOnboardStatusCancellabel: AnyCancellable? - private func setupWebView(shouldLoadInBackground: Bool) { webView.navigationDelegate = navigationDelegate webView.uiDelegate = self @@ -1456,7 +1447,7 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift } } - if navigation.url.isDuckDuckGoSearch, tunnelController?.isConnected == true { + if navigation.url.isDuckDuckGoSearch, tunnelController.isConnected == true { DailyPixel.fire(pixel: .networkProtectionEnabledOnSearch, frequency: .dailyAndCount) } } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 72d787249c..6ce1842ef4 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -41,6 +41,8 @@ protocol NetworkProtectionFeatureVisibility { func disableForWaitlistUsers() @discardableResult func disableIfUserHasNoAccess() async -> Bool + + var onboardStatusPublisher: AnyPublisher { get } } struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 1aec539d0c..f468cc4a28 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -16,14 +16,15 @@ // limitations under the License. // +import Combine +import NetworkProtection +import NetworkProtectionUI import XCTest #if SUBSCRIPTION import Subscription #endif -import NetworkProtection - @testable import DuckDuckGo_Privacy_Browser final class MoreOptionsMenuTests: XCTestCase { @@ -165,6 +166,9 @@ final class MoreOptionsMenuTests: XCTestCase { } final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { + var onboardStatusPublisher: AnyPublisher { + Just(.default).eraseToAnyPublisher() + } var isInstalled: Bool var visible: Bool From b9fa1148195733575912350cf3bd5e9b39cd22eb Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 9 Apr 2024 10:43:22 +0000 Subject: [PATCH 039/221] Bump version to 1.83.0 (154) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 06e5c06c5d..3f12085292 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 153 +CURRENT_PROJECT_VERSION = 154 From b5b07de499e3c3baa5aea99c512587e200c9354d Mon Sep 17 00:00:00 2001 From: Elle Sullivan Date: Tue, 9 Apr 2024 12:20:47 +0100 Subject: [PATCH 040/221] Add additonal error handling to DBP database and related classes (#2384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206814903024662/f Tech Design URL: CC: **Description**: Adds additional error handling mostly to the DB classes but also others. This requires updating quite a lot of classes to take this into account. Also makes the DBP tests have the same swift lint rules as the main unit tests Things this does not do: - Necessarily arrive at the final desired error handling for each class updated. Some errors are ignored with inert do/catches to allow the PR to be delivered without updating the entire codebase. - Add pixels (since they require a privacy triage) Also worth noting that sometimes this error handling introduces subtle behavior changes, where if a function uses a function that throws, it will now be rethrown, rather than the function plowing ahead regardless. I don't think there are any cases where this is an undesirable change, but worth calling out in case it turns out we were having non blocking errors that now suddenly become blocking. Long term I consider this to be a feature and part of the point. **Steps to test this PR**: 1. Obviously try the core functionality of DBP, but outside of that I don't think manual testing is the way to go with this 2. Now the other PRs are merged into this, there are some other things to test: - That error pixels are fired as expected. There are two general cases to to test: The secure vault error pixels, both from the background agent and from the main app The new "generalError" pixel (again both from the background agent and from the main app). Pay special attention to making sure it always has at least the following params: error code, error domain, and the origin function. Special care is needed here with the origin function, because if one uses pixel kit debug events it clobbers the original params - Simulate throwing various kinds of errors and make sure the processor receives them from the collection operations and fires the original pixel --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --------- Co-authored-by: Dominik Kapusta Co-authored-by: Fernando Bunn Co-authored-by: Dax the Duck Co-authored-by: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Co-authored-by: Diego Rey Mendez Co-authored-by: Pete Smith Co-authored-by: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Co-authored-by: Anh Do <18567+quanganhdo@users.noreply.github.com> Co-authored-by: Juan Manuel Pereira Co-authored-by: Christopher Brind Co-authored-by: Michal Smaga Co-authored-by: bwaresiak Co-authored-by: Mariusz Śpiewak Co-authored-by: Daniel Bernal Co-authored-by: muodov Co-authored-by: Sam Symons Co-authored-by: Alessandro Boron Co-authored-by: Federico Cappelli Co-authored-by: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Co-authored-by: Graeme Arthur Co-authored-by: amddg44 Co-authored-by: Brad Slayter Co-authored-by: Halle <378795+Halle@users.noreply.github.com> Co-authored-by: Diego Rey Mendez --- .../xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/DBP/DBPHomeViewController.swift | 17 +- .../DBP/DataBrokerProtectionAppEvents.swift | 4 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 36 ++- .../DataBrokerProtectionFeatureDisabler.swift | 6 +- ...taBrokerProtectionLoginItemScheduler.swift | 9 +- .../DBP/DataBrokerProtectionManager.swift | 2 +- ...ataBrokerProtectionBackgroundManager.swift | 41 ++- .../IPCServiceManager.swift | 27 +- .../CCF/DataBrokerProtectionErrors.swift | 3 + .../DataBrokerProtectionDataManager.swift | 88 +++--- .../DataBrokerProtectionDatabase.swift | 278 ++++++++++-------- ...erProtectionSecureVaultErrorReporter.swift | 44 +++ .../DataBrokerDatabaseBrowserViewModel.swift | 7 +- .../DataBrokerRunCustomJSONViewModel.swift | 2 +- .../DataBrokerForceOptOutViewModel.swift | 14 +- .../IPC/DataBrokerProtectionIPCClient.swift | 33 ++- .../DataBrokerProtectionIPCScheduler.swift | 9 +- .../IPC/DataBrokerProtectionIPCServer.swift | 27 +- .../Model/DBPUIViewModel.swift | 9 +- .../Model/DataBroker.swift | 20 +- .../DataBrokerOperationsCollection.swift | 35 ++- ...taBrokerProfileQueryOperationManager.swift | 42 +-- .../DataBrokerProtectionBrokerUpdater.swift | 34 ++- .../OperationPreferredDateUpdater.swift | 60 ++-- .../OperationRetriesCalculatorUseCase.swift | 8 +- .../MismatchCalculatorUseCase.swift | 9 +- .../Operations/ScanOperation.swift | 2 +- ...DataBrokerProtectionEngagementPixels.swift | 2 +- .../DataBrokerProtectionEventPixels.swift | 9 +- .../Pixels/DataBrokerProtectionPixels.swift | 21 +- ...kerProtectionStageDurationCalculator.swift | 3 + .../DataBrokerProtectionNoOpScheduler.swift | 6 +- .../DataBrokerProtectionProcessor.swift | 80 +++-- .../DataBrokerProtectionScheduler.swift | 104 ++++++- .../UI/DBPUICommunicationLayer.swift | 23 +- .../DataBrokerProtectionViewController.swift | 4 +- .../DataBrokerProtection/Tests/.swiftlint.yml | 17 ++ .../DataBrokerProtectionProfileTests.swift | 15 +- ...otectionStageDurationCalculatorTests.swift | 20 ++ .../DataBrokerProtectionTests/Mocks.swift | 4 +- .../OperationPreferredDateUpdaterTests.swift | 4 +- 42 files changed, 808 insertions(+), 372 deletions(-) create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift create mode 100644 LocalPackages/DataBrokerProtection/Tests/.swiftlint.yml diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 46c657c96d..0e02888fb7 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index fdfca10e97..c8d4c2b3ca 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -33,6 +33,7 @@ public extension Notification.Name { final class DBPHomeViewController: NSViewController { private var presentedWindowController: NSWindowController? private let dataBrokerProtectionManager: DataBrokerProtectionManager + private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() lazy var dataBrokerProtectionViewController: DataBrokerProtectionViewController = { let privacyConfigurationManager = PrivacyFeatures.contentBlocking.privacyConfigurationManager @@ -83,9 +84,14 @@ final class DBPHomeViewController: NSViewController { attachDataBrokerContainerView() } - if dataBrokerProtectionManager.dataManager.fetchProfile() != nil { - let dbpDateStore = DefaultWaitlistActivationDateStore(source: .dbp) - dbpDateStore.updateLastActiveDate() + do { + if try dataBrokerProtectionManager.dataManager.fetchProfile() != nil { + let dbpDateStore = DefaultWaitlistActivationDateStore(source: .dbp) + dbpDateStore.updateLastActiveDate() + } + } catch { + os_log("DBPHomeViewController error: viewDidLoad, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DBPHomeViewController.viewDidLoad")) } } @@ -146,6 +152,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping 0 { + if let profileQueries = try? DataBrokerProtectionManager.shared.dataManager.fetchBrokerProfileQueryData(ignoresCache: true), + profileQueries.count > 0 { restartBackgroundAgent(loginItemsManager: loginItemsManager) } else { featureVisibility.disableAndDeleteForWaitlistUsers() diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 44bbfad4b1..1478151aca 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -204,9 +204,15 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running queued operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.runQueuedOperations(showWebView: showWebView) { error in - if let error = error { - os_log("Queued operations finished, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + DataBrokerProtectionManager.shared.scheduler.runQueuedOperations(showWebView: showWebView) { errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Queued operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Queued operations finished, operation errors count: %{public}@", log: .dataBrokerProtection, operationErrors.count) + } } else { os_log("Queued operations finished", log: .dataBrokerProtection) } @@ -217,9 +223,15 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.scanAllBrokers(showWebView: showWebView) { error in - if let error = error { - os_log("Scan operations finished, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + DataBrokerProtectionManager.shared.scheduler.scanAllBrokers(showWebView: showWebView) { errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("scan operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("scan operations finished, operation errors count: %{public}@", log: .dataBrokerProtection, operationErrors.count) + } } else { os_log("Scan operations finished", log: .dataBrokerProtection) } @@ -230,9 +242,15 @@ final class DataBrokerProtectionDebugMenu: NSMenu { os_log("Running Optout operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.scheduler.optOutAllBrokers(showWebView: showWebView) { error in - if let error = error { - os_log("Optout operations finished, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + DataBrokerProtectionManager.shared.scheduler.optOutAllBrokers(showWebView: showWebView) { errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Optout operations finished, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Optout operations finished, operation errors count: %{public}@", log: .dataBrokerProtection, operationErrors.count) + } } else { os_log("Optout operations finished", log: .dataBrokerProtection) } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index b9ab9ac50a..d66ed38247 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -46,7 +46,11 @@ struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling scheduler.disableLoginItem() - dataManager.removeAllData() + do { + try dataManager.removeAllData() + } catch { + os_log("DataBrokerProtectionFeatureDisabler error: disableAndDelete, error: %{public}@", log: .error, error.localizedDescription) + } DataBrokerProtectionLoginItemPixels.fire(pixel: .dataBrokerDisableAndDeleteDaily, frequency: .dailyOnly) NotificationCenter.default.post(name: .dbpWasDisabled, object: nil) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift index feb254562a..34dafab63f 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemScheduler.swift @@ -55,7 +55,8 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler ipcScheduler.statusPublisher } - func scanAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) { + func scanAllBrokers(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { enableLoginItem() ipcScheduler.scanAllBrokers(showWebView: showWebView, completion: completion) } @@ -69,7 +70,8 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler ipcScheduler.stopScheduler() } - func optOutAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) { + func optOutAllBrokers(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { ipcScheduler.optOutAllBrokers(showWebView: showWebView, completion: completion) } @@ -77,7 +79,8 @@ extension DataBrokerProtectionLoginItemScheduler: DataBrokerProtectionScheduler ipcScheduler.runAllOperations(showWebView: showWebView) } - func runQueuedOperations(showWebView: Bool, completion: ((Error?) -> Void)?) { + func runQueuedOperations(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { ipcScheduler.runQueuedOperations(showWebView: showWebView, completion: completion) } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index 29ce721418..adb20e4aee 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -36,7 +36,7 @@ public final class DataBrokerProtectionManager { private let dataBrokerProtectionWaitlistDataSource: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp) lazy var dataManager: DataBrokerProtectionDataManager = { - let dataManager = DataBrokerProtectionDataManager(fakeBrokerFlag: fakeBrokerFlag) + let dataManager = DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) dataManager.delegate = self return dataManager }() diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift index 787a859f90..2bdc06947a 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift @@ -36,7 +36,7 @@ public final class DataBrokerProtectionBackgroundManager { private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) lazy var dataManager: DataBrokerProtectionDataManager = { - DataBrokerProtectionDataManager(fakeBrokerFlag: fakeBrokerFlag) + DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) }() lazy var scheduler: DataBrokerProtectionScheduler = { @@ -78,19 +78,36 @@ public final class DataBrokerProtectionBackgroundManager { public func runOperationsAndStartSchedulerIfPossible() { pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossible) - // If there's no saved profile we don't need to start the scheduler - if dataManager.fetchProfile() != nil { - scheduler.runQueuedOperations(showWebView: false) { [weak self] error in - guard error == nil else { - // Ideally we'd fire a pixel here, however at the moment the scheduler never ever returns an error - return - } + do { + // If there's no saved profile we don't need to start the scheduler + guard (try dataManager.fetchProfile()) != nil else { + pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile) + return + } + } catch { + pixelHandler.fire(.generalError(error: error, + functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) + return + } - self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler) - self?.scheduler.startScheduler() + scheduler.runQueuedOperations(showWebView: false) { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), error: %{public}@", + log: .dataBrokerProtection, + oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, + functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + return } - } else { - pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile) + + self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler) + self?.scheduler.startScheduler() } } } diff --git a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift index c452200f7b..3f9361e6e5 100644 --- a/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/IPCServiceManager.swift @@ -77,27 +77,30 @@ extension IPCServiceManager: IPCServerInterface { scheduler.stopScheduler() } - func optOutAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + func optOutAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { pixelHandler.fire(.ipcServerOptOutAllBrokers) - scheduler.optOutAllBrokers(showWebView: showWebView) { error in - self.pixelHandler.fire(.ipcServerOptOutAllBrokersCompletion(error: error)) - completion(error) + scheduler.optOutAllBrokers(showWebView: showWebView) { errors in + self.pixelHandler.fire(.ipcServerOptOutAllBrokersCompletion(error: errors?.oneTimeError)) + completion(errors) } } - func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + func scanAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { pixelHandler.fire(.ipcServerScanAllBrokers) - scheduler.scanAllBrokers(showWebView: showWebView) { error in - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: error)) - completion(error) + scheduler.scanAllBrokers(showWebView: showWebView) { errors in + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: errors?.oneTimeError)) + completion(errors) } } - func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + func runQueuedOperations(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { pixelHandler.fire(.ipcServerRunQueuedOperations) - scheduler.runQueuedOperations(showWebView: showWebView) { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(error) + scheduler.runQueuedOperations(showWebView: showWebView) { errors in + self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) + completion(errors) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionErrors.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionErrors.swift index 63ebe88d77..d48373aeb0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionErrors.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionErrors.swift @@ -38,6 +38,7 @@ public enum DataBrokerProtectionError: Error, Equatable, Codable { case solvingCaptchaWithCallbackError case cantCalculatePreferredRunDate case httpError(code: Int) + case dataNotInDatabase static func parse(params: Any) -> DataBrokerProtectionError { let errorDataResult = try? JSONSerialization.data(withJSONObject: params) @@ -88,6 +89,8 @@ extension DataBrokerProtectionError { return "cantCalculatePreferredRunDate" case .httpError: return "httpError" + case .dataNotInDatabase: + return "dataNotInDatabase" } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 50f5bb47fe..6ff680717b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -23,20 +23,18 @@ public protocol DataBrokerProtectionDataManaging { var cache: InMemoryDataCache { get } var delegate: DataBrokerProtectionDataManagerDelegate? { get set } - init(fakeBrokerFlag: DataBrokerDebugFlag) - func saveProfile(_ profile: DataBrokerProtectionProfile) async -> Bool - func fetchProfile(ignoresCache: Bool) -> DataBrokerProtectionProfile? - func fetchBrokerProfileQueryData(ignoresCache: Bool) async -> [BrokerProfileQueryData] - func hasMatches() -> Bool + init(pixelHandler: EventMapping, fakeBrokerFlag: DataBrokerDebugFlag) + func saveProfile(_ profile: DataBrokerProtectionProfile) async throws + func fetchProfile() throws -> DataBrokerProtectionProfile? + func prepareProfileCache() throws + func fetchBrokerProfileQueryData(ignoresCache: Bool) throws -> [BrokerProfileQueryData] + func prepareBrokerProfileQueryDataCache() throws + func hasMatches() throws -> Bool } extension DataBrokerProtectionDataManaging { - func fetchProfile() -> DataBrokerProtectionProfile? { - fetchProfile(ignoresCache: false) - } - - func fetchBrokerProfileQueryData() async -> [BrokerProfileQueryData] { - await fetchBrokerProfileQueryData(ignoresCache: false) + func fetchBrokerProfileQueryData() throws -> [BrokerProfileQueryData] { + try fetchBrokerProfileQueryData(ignoresCache: false) } } @@ -52,27 +50,31 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { internal let database: DataBrokerProtectionRepository - required public init(fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker()) { - self.database = DataBrokerProtectionDatabase(fakeBrokerFlag: fakeBrokerFlag) + required public init(pixelHandler: EventMapping, fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker()) { + self.database = DataBrokerProtectionDatabase(fakeBrokerFlag: fakeBrokerFlag, pixelHandler: pixelHandler) cache.delegate = self } - public func saveProfile(_ profile: DataBrokerProtectionProfile) async -> Bool { - let result = await database.save(profile) + public func saveProfile(_ profile: DataBrokerProtectionProfile) async throws { + do { + try await database.save(profile) + } catch { + // We should still invalidate the cache if the save fails + cache.invalidate() + throw error + } cache.invalidate() cache.profile = profile - - return result } - public func fetchProfile(ignoresCache: Bool = false) -> DataBrokerProtectionProfile? { - if !ignoresCache, cache.profile != nil { + public func fetchProfile() throws -> DataBrokerProtectionProfile? { + if cache.profile != nil { os_log("Returning cached profile", log: .dataBrokerProtection) return cache.profile } - if let profile = database.fetchProfile() { + if let profile = try database.fetchProfile() { cache.profile = profile return profile } else { @@ -81,34 +83,43 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { } } - public func fetchBrokerProfileQueryData(ignoresCache: Bool = false) async -> [BrokerProfileQueryData] { + public func prepareProfileCache() throws { + if let profile = try database.fetchProfile() { + cache.profile = profile + } else { + os_log("No profile found", log: .dataBrokerProtection) + } + } + + public func fetchBrokerProfileQueryData(ignoresCache: Bool = false) throws -> [BrokerProfileQueryData] { if !ignoresCache, !cache.brokerProfileQueryData.isEmpty { os_log("Returning cached brokerProfileQueryData", log: .dataBrokerProtection) return cache.brokerProfileQueryData } - let queryData = database.fetchAllBrokerProfileQueryData() + let queryData = try database.fetchAllBrokerProfileQueryData() cache.brokerProfileQueryData = queryData return queryData } - public func hasMatches() -> Bool { - return database.hasMatches() + public func prepareBrokerProfileQueryDataCache() throws { + cache.brokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() + } + + public func hasMatches() throws -> Bool { + return try database.hasMatches() } } extension DataBrokerProtectionDataManager: InMemoryDataCacheDelegate { - public func flushCache(profile: DataBrokerProtectionProfile?) async -> Bool { - guard let profile = profile else { return false } - let result = await saveProfile(profile) + public func saveCachedProfileToDatabase(_ profile: DataBrokerProtectionProfile) async throws { + try await saveProfile(profile) delegate?.dataBrokerProtectionDataManagerDidUpdateData() - - return result } - public func removeAllData() { - database.deleteProfileData() + public func removeAllData() throws { + try database.deleteProfileData() cache.invalidate() delegate?.dataBrokerProtectionDataManagerDidDeleteData() @@ -116,8 +127,8 @@ extension DataBrokerProtectionDataManager: InMemoryDataCacheDelegate { } public protocol InMemoryDataCacheDelegate: AnyObject { - func flushCache(profile: DataBrokerProtectionProfile?) async -> Bool - func removeAllData() + func saveCachedProfileToDatabase(_ profile: DataBrokerProtectionProfile) async throws + func removeAllData() throws } public final class InMemoryDataCache { @@ -139,10 +150,9 @@ public final class InMemoryDataCache { } extension InMemoryDataCache: DBPUICommunicationDelegate { - func saveProfile() async -> Bool { - _ = await delegate?.flushCache(profile: profile) - - return true + func saveProfile() async throws { + guard let profile = profile else { return } + try await delegate?.saveCachedProfileToDatabase(profile) } private func indexForName(matching name: DBPUIUserProfileName, in profile: DataBrokerProtectionProfile) -> Int? { @@ -178,9 +188,9 @@ extension InMemoryDataCache: DBPUICommunicationDelegate { return DBPUIUserProfile(names: names, birthYear: profile.birthYear, addresses: addresses) } - func deleteProfileData() { + func deleteProfileData() throws { profile = emptyProfile - delegate?.removeAllData() + try delegate?.removeAllData() } func addNameToCurrentUserProfile(_ name: DBPUIUserProfileName) -> Bool { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index 794b2422fc..8dbfe0b9cb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -18,176 +18,195 @@ import Foundation import Common +import SecureStorage protocol DataBrokerProtectionRepository { - func save(_ profile: DataBrokerProtectionProfile) async -> Bool - func fetchProfile() -> DataBrokerProtectionProfile? - func deleteProfileData() + func save(_ profile: DataBrokerProtectionProfile) async throws + func fetchProfile() throws -> DataBrokerProtectionProfile? + func deleteProfileData() throws - func fetchChildBrokers(for parentBroker: String) -> [DataBroker] + func fetchChildBrokers(for parentBroker: String) throws -> [DataBroker] func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws - func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) -> BrokerProfileQueryData? - func fetchAllBrokerProfileQueryData() -> [BrokerProfileQueryData] - func fetchExtractedProfiles(for brokerId: Int64) -> [ExtractedProfile] + func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) throws -> BrokerProfileQueryData? + func fetchAllBrokerProfileQueryData() throws -> [BrokerProfileQueryData] + func fetchExtractedProfiles(for brokerId: Int64) throws -> [ExtractedProfile] - func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) - func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) - func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) - func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) - func updateRemovedDate(_ date: Date?, on extractedProfileId: Int64) + func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws + func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws + func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws + func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws + func updateRemovedDate(_ date: Date?, on extractedProfileId: Int64) throws - func add(_ historyEvent: HistoryEvent) - func fetchLastEvent(brokerId: Int64, profileQueryId: Int64) -> HistoryEvent? - func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) -> [HistoryEvent] - func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) -> [HistoryEvent] - func hasMatches() -> Bool + func add(_ historyEvent: HistoryEvent) throws + func fetchLastEvent(brokerId: Int64, profileQueryId: Int64) throws -> HistoryEvent? + func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) throws -> [HistoryEvent] + func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> [HistoryEvent] + func hasMatches() throws -> Bool - func fetchAttemptInformation(for extractedProfileId: Int64) -> AttemptInformation? - func addAttempt(extractedProfileId: Int64, attemptUUID: UUID, dataBroker: String, lastStageDate: Date, startTime: Date) + func fetchAttemptInformation(for extractedProfileId: Int64) throws -> AttemptInformation? + func addAttempt(extractedProfileId: Int64, attemptUUID: UUID, dataBroker: String, lastStageDate: Date, startTime: Date) throws } final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { private static let profileId: Int64 = 1 // At the moment, we only support one profile for DBP. private let fakeBrokerFlag: DataBrokerDebugFlag + private let pixelHandler: EventMapping private let vault: (any DataBrokerProtectionSecureVault)? + private let secureVaultErrorReporter: SecureVaultErrorReporting? - init(fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker(), vault: (any DataBrokerProtectionSecureVault)? = nil) { + init(fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker(), + pixelHandler: EventMapping, + vault: (any DataBrokerProtectionSecureVault)? = nil, + secureVaultErrorReporter: SecureVaultErrorReporting? = DataBrokerProtectionSecureVaultErrorReporter.shared) { self.fakeBrokerFlag = fakeBrokerFlag + self.pixelHandler = pixelHandler self.vault = vault + self.secureVaultErrorReporter = secureVaultErrorReporter } - func save(_ profile: DataBrokerProtectionProfile) async -> Bool { + func save(_ profile: DataBrokerProtectionProfile) async throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) if try vault.fetchProfile(with: Self.profileId) != nil { try await updateProfile(profile, vault: vault) } else { try await saveNewProfile(profile, vault: vault) } - - return true } catch { - os_log("Database error: saveProfile, error: %{public}@", log: .error, error.localizedDescription) - - return false + os_log("Database error: save profile, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.save profile")) + throw error } } - public func fetchProfile() -> DataBrokerProtectionProfile? { + public func fetchProfile() throws -> DataBrokerProtectionProfile? { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) return try vault.fetchProfile(with: Self.profileId) } catch { os_log("Database error: fetchProfile, error: %{public}@", log: .error, error.localizedDescription) - return nil + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchProfile")) + throw error } } - public func deleteProfileData() { + public func deleteProfileData() throws { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.deleteProfileData() } catch { - os_log("Database error: removeProfileData, error: %{public}@", log: .error, error.localizedDescription) - return + os_log("Database error: deleteProfileData, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.deleteProfileData")) + throw error } } - func fetchChildBrokers(for parentBroker: String) -> [DataBroker] { + func fetchChildBrokers(for parentBroker: String) throws -> [DataBroker] { do { - let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) return try vault.fetchChildBrokers(for: parentBroker) } catch { os_log("Database error: fetchChildBrokers, error: %{public}@", log: .error, error.localizedDescription) - return [DataBroker]() + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchChildBrokers for parentBroker")) + throw error } } func save(_ extractedProfile: ExtractedProfile, brokerId: Int64, profileQueryId: Int64) throws -> Int64 { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + do { + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) - return try vault.save(extractedProfile: extractedProfile, brokerId: brokerId, profileQueryId: profileQueryId) + return try vault.save(extractedProfile: extractedProfile, brokerId: brokerId, profileQueryId: profileQueryId) + } catch { + os_log("Database error: extractedProfile, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.save extractedProfile brokerId profileQueryId")) + throw error + } } - func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) -> BrokerProfileQueryData? { + func brokerProfileQueryData(for brokerId: Int64, and profileQueryId: Int64) throws -> BrokerProfileQueryData? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) - if let broker = try vault.fetchBroker(with: brokerId), - let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), - let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) { - - let optOutOperations = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) - - return BrokerProfileQueryData( - dataBroker: broker, - profileQuery: profileQuery, - scanOperationData: scanOperation, - optOutOperationsData: optOutOperations - ) - } else { - // We should throw here. The caller probably needs to know that some of the - // models he was looking for where not found. This will be worked on the error handling task/project. - return nil + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + guard let broker = try vault.fetchBroker(with: brokerId), + let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), + let scanOperation = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { + let error = DataBrokerProtectionError.dataNotInDatabase + os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.brokerProfileQueryData for brokerId and profileQueryId")) + throw error } + + let optOutOperations = try vault.fetchOptOuts(brokerId: brokerId, profileQueryId: profileQueryId) + + return BrokerProfileQueryData( + dataBroker: broker, + profileQuery: profileQuery, + scanOperationData: scanOperation, + optOutOperationsData: optOutOperations + ) } catch { os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) - return nil + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.brokerProfileQueryData for brokerId and profileQueryId")) + throw error } } - func fetchExtractedProfiles(for brokerId: Int64) -> [ExtractedProfile] { + func fetchExtractedProfiles(for brokerId: Int64) throws -> [ExtractedProfile] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) - + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) return try vault.fetchExtractedProfiles(for: brokerId) } catch { - os_log("Database error: fetchExtractedProfiles for scan, error: %{public}@", log: .error, error.localizedDescription) - return [ExtractedProfile]() + os_log("Database error: fetchExtractedProfiles, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchExtractedProfiles for brokerId")) + throw error } } - func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) { + func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) - + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } catch { - os_log("Database error: updatePreferredRunDate for scan, error: %{public}@", log: .error, error.localizedDescription) + os_log("Database error: updatePreferredRunDate without extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updatePreferredRunDate date brokerID profileQueryId")) + throw error } } - func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) { + func updatePreferredRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.updatePreferredRunDate( date, brokerId: brokerId, profileQueryId: profileQueryId, - extractedProfileId: extractedProfileId - ) + extractedProfileId: extractedProfileId) } catch { - os_log("Database error: updatePreferredRunDate for optOut, error: %{public}@", log: .error, error.localizedDescription) + os_log("Database error: updatePreferredRunDate with extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updatePreferredRunDate date brokerID profileQueryId extractedProfileID")) + throw error } } - func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) { + func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) - + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.updateLastRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } catch { - os_log("Database error: updateLastRunDate for scan, error: %{public}@", log: .error, error.localizedDescription) + os_log("Database error: updateLastRunDate without extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateLastRunDate date brokerID profileQueryId")) + throw error } } - func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) { + func updateLastRunDate(_ date: Date?, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.updateLastRunDate( date, @@ -196,23 +215,26 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { extractedProfileId: extractedProfileId ) } catch { - os_log("Database error: updateLastRunDate for optOut, error: %{public}@", log: .error, error.localizedDescription) + os_log("Database error: updateLastRunDate with extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateLastRunDate date brokerId profileQueryId extractedProfileId")) + throw error } } - func updateRemovedDate(_ date: Date?, on extractedProfileId: Int64) { + func updateRemovedDate(_ date: Date?, on extractedProfileId: Int64) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) - + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.updateRemovedDate(for: extractedProfileId, with: date) } catch { - os_log("Database error: updateRemoveDate, error: %{public}@", log: .error, error.localizedDescription) + os_log("Database error: updateRemovedDate, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateRemovedDate date on extractedProfileId")) + throw error } } - func add(_ historyEvent: HistoryEvent) { + func add(_ historyEvent: HistoryEvent) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) if let extractedProfileId = historyEvent.extractedProfileId { try vault.save(historyEvent: historyEvent, brokerId: historyEvent.brokerId, profileQueryId: historyEvent.profileQueryId, extractedProfileId: extractedProfileId) @@ -220,13 +242,15 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { try vault.save(historyEvent: historyEvent, brokerId: historyEvent.brokerId, profileQueryId: historyEvent.profileQueryId) } } catch { - os_log("Database error: addHistoryEvent, error: %{public}@", log: .error, error.localizedDescription) + os_log("Database error: add historyEvent, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.add historyEvent")) + throw error } } - func fetchAllBrokerProfileQueryData() -> [BrokerProfileQueryData] { + func fetchAllBrokerProfileQueryData() throws -> [BrokerProfileQueryData] { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) let brokers = try vault.fetchAllBrokers() let profileQueries = try vault.fetchAllProfileQueries(for: Self.profileId) var brokerProfileQueryDataList = [BrokerProfileQueryData]() @@ -252,33 +276,52 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return brokerProfileQueryDataList } catch { os_log("Database error: fetchAllBrokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) - return [BrokerProfileQueryData]() + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchAllBrokerProfileQueryData")) + throw error } } func saveOptOutOperation(optOut: OptOutOperationData, extractedProfile: ExtractedProfile) throws { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + do { + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) - try vault.save(brokerId: optOut.brokerId, - profileQueryId: optOut.profileQueryId, - extractedProfile: extractedProfile, - lastRunDate: optOut.lastRunDate, - preferredRunDate: optOut.preferredRunDate) + try vault.save(brokerId: optOut.brokerId, + profileQueryId: optOut.profileQueryId, + extractedProfile: extractedProfile, + lastRunDate: optOut.lastRunDate, + preferredRunDate: optOut.preferredRunDate) + } catch { + os_log("Database error: saveOptOutOperation, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.saveOptOutOperation optOut extractedProfile")) + throw error + } } - func fetchLastEvent(brokerId: Int64, profileQueryId: Int64) -> HistoryEvent? { + func fetchLastEvent(brokerId: Int64, profileQueryId: Int64) throws -> HistoryEvent? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) let events = try vault.fetchEvents(brokerId: brokerId, profileQueryId: profileQueryId) return events.max(by: { $0.date < $1.date }) } catch { os_log("Database error: fetchLastEvent, error: %{public}@", log: .error, error.localizedDescription) - return nil + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "fetchLastEvent brokerId profileQueryId")) + throw error + } + } + + func hasMatches() throws -> Bool { + do { + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) + return try vault.hasMatches() + } catch { + os_log("Database error: hasMatches, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.hasMatches")) + throw error } } - func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) -> [HistoryEvent] { + func fetchScanHistoryEvents(brokerId: Int64, profileQueryId: Int64) throws -> [HistoryEvent] { do { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) guard let scan = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { @@ -288,11 +331,12 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return scan.historyEvents } catch { os_log("Database error: fetchHistoryEvents, error: %{public}@", log: .error, error.localizedDescription) - return [HistoryEvent]() + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "fetchScanHistoryEvents brokerId profileQueryId")) + throw error } } - func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) -> [HistoryEvent] { + func fetchOptOutHistoryEvents(brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> [HistoryEvent] { do { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) guard let optOut = try vault.fetchOptOut(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) else { @@ -302,33 +346,25 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return optOut.historyEvents } catch { os_log("Database error: fetchHistoryEvents, error: %{public}@", log: .error, error.localizedDescription) - return [HistoryEvent]() + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchOptOutHistoryEvents brokerId profileQueryId extractedProfileId")) + throw error } } - func hasMatches() -> Bool { + func fetchAttemptInformation(for extractedProfileId: Int64) throws -> AttemptInformation? { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) - return try vault.hasMatches() - } catch { - os_log("Database error: wereThereAnyMatches, error: %{public}@", log: .error, error.localizedDescription) - return false - } - } - - func fetchAttemptInformation(for extractedProfileId: Int64) -> AttemptInformation? { - do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) return try vault.fetchAttemptInformation(for: extractedProfileId) } catch { os_log("Database error: fetchAttemptInformation, error: %{public}@", log: .error, error.localizedDescription) - return nil + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchAttemptInformation for extractedProfileId")) + throw error } } - func addAttempt(extractedProfileId: Int64, attemptUUID: UUID, dataBroker: String, lastStageDate: Date, startTime: Date) { + func addAttempt(extractedProfileId: Int64, attemptUUID: UUID, dataBroker: String, lastStageDate: Date, startTime: Date) throws { do { - let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) + let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: secureVaultErrorReporter) try vault.save(extractedProfileId: extractedProfileId, attemptUUID: attemptUUID, dataBroker: dataBroker, @@ -336,6 +372,8 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { startTime: startTime) } catch { os_log("Database error: addAttempt, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.addAttempt extractedProfileId attemptUUID dataBroker lastStageDate startTime")) + throw error } } } @@ -365,7 +403,7 @@ extension DataBrokerProtectionDatabase { let newProfileQueries = profile.profileQueries _ = try vault.save(profile: profile) - if let brokers = FileResources().fetchBrokerFromResourceFiles() { + if let brokers = try FileResources().fetchBrokerFromResourceFiles() { var brokerIDs = [Int64]() for broker in brokers { @@ -387,7 +425,7 @@ extension DataBrokerProtectionDatabase { let newProfileQueries = profile.profileQueries - let databaseBrokerProfileQueryData = fetchAllBrokerProfileQueryData() + let databaseBrokerProfileQueryData = try fetchAllBrokerProfileQueryData() let databaseProfileQueries = databaseBrokerProfileQueryData.map { $0.profileQuery } // The queries we need to create are the one that exist on the new ones but not in the database @@ -462,7 +500,7 @@ extension DataBrokerProtectionDatabase { if !profile.deprecated { for brokerID in brokerIDs where !profile.deprecated { - updatePreferredRunDate(Date(), brokerId: brokerID, profileQueryId: profileQueryID) + try updatePreferredRunDate(Date(), brokerId: brokerID, profileQueryId: profileQueryID) } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift new file mode 100644 index 0000000000..21cb003862 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionSecureVaultErrorReporter.swift @@ -0,0 +1,44 @@ +// +// DataBrokerProtectionSecureVaultErrorReporter.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 SecureStorage +import PixelKit +import Common + +final class DataBrokerProtectionSecureVaultErrorReporter: SecureVaultErrorReporting { + static let shared = DataBrokerProtectionSecureVaultErrorReporter(pixelHandler: DataBrokerProtectionPixelsHandler()) + + let pixelHandler: EventMapping + private init(pixelHandler: EventMapping) { + self.pixelHandler = pixelHandler + } + + func secureVaultInitFailed(_ error: SecureStorageError) { + switch error { + case .initFailed, .failedToOpenDatabase: + pixelHandler.fire(.secureVaultInitError(error: error)) + default: + pixelHandler.fire(.secureVaultError(error: error)) + } + + // Also fire the general pixel, since for now all errors are kept under one pixel + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "secureVaultErrorReporter")) + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift index 514957192a..a3bf0d1547 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBaseBrowser/DataBrokerDatabaseBrowserViewModel.swift @@ -31,7 +31,7 @@ final class DataBrokerDatabaseBrowserViewModel: ObservableObject { self.selectedTable = tables.first self.dataManager = nil } else { - self.dataManager = DataBrokerProtectionDataManager() + self.dataManager = DataBrokerProtectionDataManager(pixelHandler: DataBrokerProtectionPixelsHandler()) self.tables = [DataBrokerDatabaseBrowserData.Table]() self.selectedTable = nil updateTables() @@ -48,7 +48,10 @@ final class DataBrokerDatabaseBrowserViewModel: ObservableObject { guard let dataManager = self.dataManager else { return } Task { - let data = await dataManager.fetchBrokerProfileQueryData(ignoresCache: true) + guard let data = try? dataManager.fetchBrokerProfileQueryData(ignoresCache: true) else { + assertionFailure("DataManager error during DataBrokerDatavaseBrowserViewModel.updateTables") + return + } let profileBrokers = data.map { $0.dataBroker } let dataBrokers = Array(Set(profileBrokers)).sorted { $0.id ?? 0 < $1.id ?? 0 } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 3e0d151dc9..e7b72fb99b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -174,7 +174,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { self.contentScopeProperties = contentScopeProperties let fileResources = FileResources() - self.brokers = fileResources.fetchBrokerFromResourceFiles() ?? [DataBroker]() + self.brokers = (try? fileResources.fetchBrokerFromResourceFiles()) ?? [DataBroker]() } func runAllBrokers() { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift index 3668052d2f..322af7070d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/ForceOptOut/DataBrokerForceOptOutViewModel.swift @@ -23,14 +23,18 @@ final class DataBrokerForceOptOutViewModel: ObservableObject { private let dataManager: DataBrokerProtectionDataManager @Published var optOutData = [OptOutViewData]() - internal init(dataManager: DataBrokerProtectionDataManager = DataBrokerProtectionDataManager()) { + internal init(dataManager: DataBrokerProtectionDataManager = + DataBrokerProtectionDataManager(pixelHandler: DataBrokerProtectionPixelsHandler())) { self.dataManager = dataManager loadNotRemovedOptOutData() } private func loadNotRemovedOptOutData() { Task { @MainActor in - let brokerProfileData = await dataManager.fetchBrokerProfileQueryData(ignoresCache: true) + guard let brokerProfileData = try? dataManager.fetchBrokerProfileQueryData(ignoresCache: true) else { + assertionFailure() + return + } self.optOutData = brokerProfileData .flatMap { profileData in profileData.optOutOperationsData.map { ($0, profileData.dataBroker.name) } @@ -70,6 +74,10 @@ struct OptOutViewData: Identifiable { private extension DataBrokerProtectionDataManager { func setAsRemoved(_ extractedProfileID: Int64) { - self.database.updateRemovedDate(Date(), on: extractedProfileID) + do { + try self.database.updateRemovedDate(Date(), on: extractedProfileID) + } catch { + assertionFailure() + } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 2aa3953437..f651a62440 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -102,42 +102,45 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func optOutAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + public func optOutAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { self.pixelHandler.fire(.ipcServerOptOutAllBrokers) xpc.execute(call: { server in - server.optOutAllBrokers(showWebView: showWebView) { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(error) + server.optOutAllBrokers(showWebView: showWebView) { errors in + self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) + completion(errors) } }, xpcReplyErrorHandler: { error in self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(error) + completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) }) } - public func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + public func scanAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { self.pixelHandler.fire(.ipcServerScanAllBrokers) xpc.execute(call: { server in - server.scanAllBrokers(showWebView: showWebView) { error in - self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: error)) - completion(error) + server.scanAllBrokers(showWebView: showWebView) { errors in + self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: errors?.oneTimeError)) + completion(errors) } }, xpcReplyErrorHandler: { error in self.pixelHandler.fire(.ipcServerScanAllBrokersCompletion(error: error)) - completion(error) + completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) }) } - public func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + public func runQueuedOperations(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { self.pixelHandler.fire(.ipcServerRunQueuedOperations) xpc.execute(call: { server in - server.runQueuedOperations(showWebView: showWebView) { error in - self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(error) + server.runQueuedOperations(showWebView: showWebView) { errors in + self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: errors?.oneTimeError)) + completion(errors) } }, xpcReplyErrorHandler: { error in self.pixelHandler.fire(.ipcServerRunQueuedOperationsCompletion(error: error)) - completion(error) + completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) }) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift index b69402626d..6f9bb8aa9f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCScheduler.swift @@ -46,17 +46,20 @@ public final class DataBrokerProtectionIPCScheduler: DataBrokerProtectionSchedul ipcClient.stopScheduler() } - public func optOutAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) { + public func optOutAllBrokers(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } ipcClient.optOutAllBrokers(showWebView: showWebView, completion: completion) } - public func scanAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) { + public func scanAllBrokers(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } ipcClient.scanAllBrokers(showWebView: showWebView, completion: completion) } - public func runQueuedOperations(showWebView: Bool, completion: ((Error?) -> Void)?) { + public func runQueuedOperations(showWebView: Bool, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { let completion = completion ?? { _ in } ipcClient.runQueuedOperations(showWebView: showWebView, completion: completion) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index a2bc3d0e56..33594703cb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -38,9 +38,12 @@ public protocol IPCServerInterface: AnyObject { /// func stopScheduler() - func optOutAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) - func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) - func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) + func optOutAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func scanAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func runQueuedOperations(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runAllOperations(showWebView: Bool) // MARK: - Debugging Features @@ -73,9 +76,12 @@ protocol XPCServerInterface { /// func stopScheduler() - func optOutAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) - func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) - func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) + func optOutAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func scanAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) + func runQueuedOperations(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) func runAllOperations(showWebView: Bool) // MARK: - Debugging Features @@ -143,15 +149,18 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { serverDelegate?.stopScheduler() } - func optOutAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + func optOutAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { serverDelegate?.optOutAllBrokers(showWebView: showWebView, completion: completion) } - func scanAllBrokers(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + func scanAllBrokers(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { serverDelegate?.scanAllBrokers(showWebView: showWebView, completion: completion) } - func runQueuedOperations(showWebView: Bool, completion: @escaping ((Error?) -> Void)) { + func runQueuedOperations(showWebView: Bool, + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { serverDelegate?.runQueuedOperations(showWebView: showWebView, completion: completion) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index ef1db86240..570a8f1e60 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -20,6 +20,7 @@ import Foundation import Combine import WebKit import BrowserServicesKit +import Common protocol DBPUIScanOps: AnyObject { func startScan() -> Bool @@ -35,6 +36,7 @@ final class DBPUIViewModel { private var communicationLayer: DBPUICommunicationLayer? private var webView: WKWebView? private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable + private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() init(dataManager: DataBrokerProtectionDataManaging, scheduler: DataBrokerProtectionScheduler, @@ -77,6 +79,11 @@ extension DBPUIViewModel: DBPUIScanOps { } func updateCacheWithCurrentScans() async { - _ = await dataManager.fetchBrokerProfileQueryData(ignoresCache: true) + do { + try dataManager.prepareBrokerProfileQueryDataCache() + } catch { + os_log("DBPUIViewModel error: updateCacheWithCurrentScans, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DBPUIViewModel.updateCacheWithCurrentScans")) + } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index 5408f88ea7..41a5293846 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -17,6 +17,7 @@ // import Foundation +import Common struct DataBrokerScheduleConfig: Codable { let retryError: Int @@ -172,14 +173,17 @@ struct DataBroker: Codable, Sendable { return optOutType == .parentSiteOptOut } - static func initFromResource(_ url: URL) -> DataBroker { - // swiftlint:disable:next force_try - let data = try! Data(contentsOf: url) - let jsonDecoder = JSONDecoder() - jsonDecoder.dateDecodingStrategy = .millisecondsSince1970 - // swiftlint:disable:next force_try - let broker = try! jsonDecoder.decode(DataBroker.self, from: data) - return broker + static func initFromResource(_ url: URL) throws -> DataBroker { + do { + let data = try Data(contentsOf: url) + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .millisecondsSince1970 + let broker = try jsonDecoder.decode(DataBroker.self, from: data) + return broker + } catch { + os_log("DataBroker error: initFromResource, error: %{public}@", log: .error, error.localizedDescription) + throw error + } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift index d77c7c7d6b..78b76af560 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperationsCollection.swift @@ -19,6 +19,15 @@ import Foundation import Common +protocol DataBrokerOperationsCollectionErrorDelegate: AnyObject { + func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, + didError error: Error, + whileRunningBrokerOperationData: BrokerOperationData, + withDataBrokerName dataBrokerName: String?) + func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, + didErrorBeforeStartingBrokerOperations error: Error) +} + final class DataBrokerOperationsCollection: Operation { enum OperationType { @@ -27,6 +36,9 @@ final class DataBrokerOperationsCollection: Operation { case all } + public var error: Error? + public weak var errorDelegate: DataBrokerOperationsCollectionErrorDelegate? + private let dataBrokerID: Int64 private let database: DataBrokerProtectionRepository private let id = UUID() @@ -126,8 +138,18 @@ final class DataBrokerOperationsCollection: Operation { return filteredAndSortedOperationsData } + // swiftlint:disable:next function_body_length private func runOperation() async { - let allBrokerProfileQueryData = database.fetchAllBrokerProfileQueryData() + let allBrokerProfileQueryData: [BrokerProfileQueryData] + + do { + allBrokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() + } catch { + os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) + errorDelegate?.dataBrokerOperationsCollection(self, didErrorBeforeStartingBrokerOperations: error) + return + } + let brokerProfileQueriesData = allBrokerProfileQueryData.filter { $0.dataBroker.id == dataBrokerID } let filteredAndSortedOperationsData = filterAndSortOperationsData(brokerProfileQueriesData: brokerProfileQueriesData, @@ -174,12 +196,11 @@ final class DataBrokerOperationsCollection: Operation { } catch { os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - if let error = error as? DataBrokerProtectionError, - let dataBrokerName = brokerProfileQueriesData.first?.dataBroker.name { - pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) - } else { - os_log("Cant handle error", log: .dataBrokerProtection) - } + self.error = error + errorDelegate?.dataBrokerOperationsCollection(self, + didError: error, + whileRunningBrokerOperationData: operationData, + withDataBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index f5c29ae8b7..f1e9a7b377 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -112,7 +112,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } defer { - database.updateLastRunDate(Date(), brokerId: brokerId, profileQueryId: profileQueryId) + try? database.updateLastRunDate(Date(), brokerId: brokerId, profileQueryId: profileQueryId) os_log("Finished scan operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) notificationCenter.post(name: DataBrokerProtectionNotifications.didFinishScan, object: brokerProfileQueryData.dataBroker.name) } @@ -122,7 +122,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) - database.add(event) + try database.add(event) let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: stageCalculator, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) os_log("Extracted profiles: %@", log: .dataBrokerProtection, extractedProfiles) @@ -130,12 +130,12 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { if !extractedProfiles.isEmpty { stageCalculator.fireScanSuccess(matchesFound: extractedProfiles.count) let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .matchesFound(count: extractedProfiles.count)) - database.add(event) + try database.add(event) for extractedProfile in extractedProfiles { // We check if the profile exists in the database. - let extractedProfilesForBroker = database.fetchExtractedProfiles(for: brokerId) + let extractedProfilesForBroker = try database.fetchExtractedProfiles(for: brokerId) let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier } // If the profile exists we do not create a new opt-out operation @@ -144,8 +144,8 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { if alreadyInDatabaseProfile.removedDate != nil { let reAppereanceEvent = HistoryEvent(extractedProfileId: extractedProfile.id, brokerId: brokerId, profileQueryId: profileQueryId, type: .reAppearence) eventPixels.fireReAppereanceEventPixel() - database.add(reAppereanceEvent) - database.updateRemovedDate(nil, on: id) + try database.add(reAppereanceEvent) + try database.updateRemovedDate(nil, on: id) } os_log("Extracted profile already exists in database: %@", log: .dataBrokerProtection, id.description) @@ -175,7 +175,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } else { stageCalculator.fireScanFailed() let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .noMatchFound) - database.add(event) + try database.add(event) } // Check for removed profiles @@ -190,8 +190,8 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { for removedProfile in removedProfiles { if let extractedProfileId = removedProfile.id { let event = HistoryEvent(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutConfirmed) - database.add(event) - database.updateRemovedDate(Date(), on: extractedProfileId) + try database.add(event) + try database.updateRemovedDate(Date(), on: extractedProfileId) shouldSendProfileRemovedNotification = true try updateOperationDataDates( origin: .scan, @@ -204,7 +204,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { os_log("Profile removed from optOutsData: %@", log: .dataBrokerProtection, String(describing: removedProfile)) - if let attempt = database.fetchAttemptInformation(for: extractedProfileId), let attemptUUID = UUID(uuidString: attempt.attemptId) { + if let attempt = try database.fetchAttemptInformation(for: extractedProfileId), let attemptUUID = UUID(uuidString: attempt.attemptId) { let now = Date() let calculateDurationSinceLastStage = now.timeIntervalSince(attempt.lastStageDate) * 1000 let calculateDurationSinceStart = now.timeIntervalSince(attempt.startDate) * 1000 @@ -242,9 +242,9 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } private func sendProfileRemovedNotificationIfNecessary(userNotificationService: DataBrokerProtectionUserNotificationService, database: DataBrokerProtectionRepository) { - let savedExtractedProfiles = database.fetchAllBrokerProfileQueryData().flatMap { $0.extractedProfiles } - guard savedExtractedProfiles.count > 0 else { + guard let savedExtractedProfiles = try? database.fetchAllBrokerProfileQueryData().flatMap({ $0.extractedProfiles }), + savedExtractedProfiles.count > 0 else { return } @@ -292,7 +292,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { defer { os_log("Finished opt-out operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) - database.updateLastRunDate(Date(), brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) + try? database.updateLastRunDate(Date(), brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) do { try updateOperationDataDates( origin: .optOut, @@ -317,7 +317,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } do { - database.add(.init(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutStarted)) + try database.add(.init(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutStarted)) try await runner.optOut(profileQuery: brokerProfileQueryData, extractedProfile: extractedProfile, @@ -325,23 +325,23 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) - let tries = retriesCalculatorUseCase.calculateForOptOut(database: database, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) + let tries = try retriesCalculatorUseCase.calculateForOptOut(database: database, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) stageDurationCalculator.fireOptOutValidate() stageDurationCalculator.fireOptOutSubmitSuccess(tries: tries) let updater = OperationPreferredDateUpdaterUseCase(database: database) - updater.updateChildrenBrokerForParentBroker(brokerProfileQueryData.dataBroker, + try updater.updateChildrenBrokerForParentBroker(brokerProfileQueryData.dataBroker, profileQueryId: profileQueryId) - database.addAttempt(extractedProfileId: extractedProfileId, + try database.addAttempt(extractedProfileId: extractedProfileId, attemptUUID: stageDurationCalculator.attemptId, dataBroker: stageDurationCalculator.dataBroker, lastStageDate: stageDurationCalculator.lastStateTime, startTime: stageDurationCalculator.startTime) - database.add(.init(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)) + try database.add(.init(extractedProfileId: extractedProfileId, brokerId: brokerId, profileQueryId: profileQueryId, type: .optOutRequested)) } catch { - let tries = retriesCalculatorUseCase.calculateForOptOut(database: database, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) - stageDurationCalculator.fireOptOutFailure(tries: tries) + let tries = try? retriesCalculatorUseCase.calculateForOptOut(database: database, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) + stageDurationCalculator.fireOptOutFailure(tries: tries ?? -1) handleOperationError( origin: .optOut, brokerId: brokerId, @@ -394,7 +394,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } } - database.add(event) + try? database.add(event) do { try updateOperationDataDates( diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index f3203334e2..79020cde0b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -20,20 +20,26 @@ import Foundation import Common protocol ResourcesRepository { - func fetchBrokerFromResourceFiles() -> [DataBroker]? + func fetchBrokerFromResourceFiles() throws -> [DataBroker]? } final class FileResources: ResourcesRepository { + enum FileResourcesError: Error { + case bundleResourceURLNil + } + private let fileManager: FileManager init(fileManager: FileManager = .default) { self.fileManager = fileManager } - func fetchBrokerFromResourceFiles() -> [DataBroker]? { + func fetchBrokerFromResourceFiles() throws -> [DataBroker]? { guard let resourceURL = Bundle.module.resourceURL else { - return nil + assertionFailure() + os_log("DataBrokerProtectionUpdater: error FileResources fetchBrokerFromResourceFiles, error: Bundle.module.resourceURL is nil", log: .error) + throw FileResourcesError.bundleResourceURLNil } do { @@ -47,10 +53,10 @@ final class FileResources: ResourcesRepository { $0.isJSON && !$0.hasFakePrefix } - return brokerJSONFiles.map(DataBroker.initFromResource(_:)) + return try brokerJSONFiles.map(DataBroker.initFromResource(_:)) } catch { - os_log("Error fetching brokers JSON files from resources", log: .dataBrokerProtection) - return nil + os_log("DataBrokerProtectionUpdater: error FileResources error: fetchBrokerFromResourceFiles, error: %{public}@", log: .error, error.localizedDescription) + throw error } } } @@ -97,15 +103,18 @@ public struct DataBrokerProtectionBrokerUpdater { private let resources: ResourcesRepository private let vault: any DataBrokerProtectionSecureVault private let appVersion: AppVersionNumberProvider + private let pixelHandler: EventMapping init(repository: BrokerUpdaterRepository = BrokerUpdaterUserDefaults(), resources: ResourcesRepository = FileResources(), vault: any DataBrokerProtectionSecureVault, - appVersion: AppVersionNumberProvider = AppVersionNumber()) { + appVersion: AppVersionNumberProvider = AppVersionNumber(), + pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler()) { self.repository = repository self.resources = resources self.vault = vault self.appVersion = appVersion + self.pixelHandler = pixelHandler } public static func provide() -> DataBrokerProtectionBrokerUpdater? { @@ -118,13 +127,22 @@ public struct DataBrokerProtectionBrokerUpdater { } public func updateBrokers() { - guard let brokers = resources.fetchBrokerFromResourceFiles() else { return } + let brokers: [DataBroker]? + do { + brokers = try resources.fetchBrokerFromResourceFiles() + } catch { + os_log("DataBrokerProtectionBrokerUpdater updateBrokers, error: %{public}@", log: .error, error.localizedDescription) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionBrokerUpdater.updateBrokers")) + return + } + guard let brokers = brokers else { return } for broker in brokers { do { try update(broker) } catch { os_log("Error updating broker: %{public}@, with version: %{public}@", log: .dataBrokerProtection, broker.name, broker.version) + pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionBrokerUpdater.updateBrokers")) } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift index 1b27e2025d..73cb7fb2b1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift @@ -33,7 +33,7 @@ protocol OperationPreferredDateUpdater { extractedProfileId: Int64?, schedulingConfig: DataBrokerScheduleConfig) throws - func updateChildrenBrokerForParentBroker(_ parentBroker: DataBroker, profileQueryId: Int64) + func updateChildrenBrokerForParentBroker(_ parentBroker: DataBroker, profileQueryId: Int64) throws } struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { @@ -47,8 +47,8 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { extractedProfileId: Int64?, schedulingConfig: DataBrokerScheduleConfig) throws { - guard let brokerProfileQuery = database.brokerProfileQueryData(for: brokerId, - and: profileQueryId) else { return } + guard let brokerProfileQuery = try database.brokerProfileQueryData(for: brokerId, + and: profileQueryId) else { return } try updateScanOperationDataDates(origin: origin, brokerId: brokerId, @@ -70,16 +70,21 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { /// 1, This method fetches scan operations with the profileQueryId and with child sites of parentBrokerId /// 2. Then for each one it updates the preferredRunDate of the scan to its confirm scan - func updateChildrenBrokerForParentBroker(_ parentBroker: DataBroker, profileQueryId: Int64) { - let childBrokers = database.fetchChildBrokers(for: parentBroker.name) - - childBrokers.forEach { childBroker in - if let childBrokerId = childBroker.id { - let confirmOptOutScanDate = Date().addingTimeInterval(childBroker.schedulingConfig.confirmOptOutScan.hoursToSeconds) - database.updatePreferredRunDate(confirmOptOutScanDate, - brokerId: childBrokerId, - profileQueryId: profileQueryId) + func updateChildrenBrokerForParentBroker(_ parentBroker: DataBroker, profileQueryId: Int64) throws { + do { + let childBrokers = try database.fetchChildBrokers(for: parentBroker.name) + + try childBrokers.forEach { childBroker in + if let childBrokerId = childBroker.id { + let confirmOptOutScanDate = Date().addingTimeInterval(childBroker.schedulingConfig.confirmOptOutScan.hoursToSeconds) + try database.updatePreferredRunDate(confirmOptOutScanDate, + brokerId: childBrokerId, + profileQueryId: profileQueryId) + } } + } catch { + os_log("OperationPreferredDateUpdaterUseCase error: updateChildrenBrokerForParentBroker, error: %{public}@", log: .error, error.localizedDescription) + throw error } } @@ -102,10 +107,10 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } if newScanPreferredRunDate != currentScanPreferredRunDate { - updatePreferredRunDate(newScanPreferredRunDate, - brokerId: brokerId, - profileQueryId: profileQueryId, - extractedProfileId: nil) + try updatePreferredRunDate(newScanPreferredRunDate, + brokerId: brokerId, + profileQueryId: profileQueryId, + extractedProfileId: nil) } } @@ -129,10 +134,10 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } if newOptOutPreferredDate != currentOptOutPreferredRunDate { - updatePreferredRunDate(newOptOutPreferredDate, - brokerId: brokerId, - profileQueryId: profileQueryId, - extractedProfileId: extractedProfileId) + try updatePreferredRunDate(newOptOutPreferredDate, + brokerId: brokerId, + profileQueryId: profileQueryId, + extractedProfileId: extractedProfileId) } } @@ -146,11 +151,16 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { private func updatePreferredRunDate( _ date: Date?, brokerId: Int64, profileQueryId: Int64, - extractedProfileId: Int64?) { - if let extractedProfileId = extractedProfileId { - database.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) - } else { - database.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) + extractedProfileId: Int64?) throws { + do { + if let extractedProfileId = extractedProfileId { + try database.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) + } else { + try database.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) + } + } catch { + os_log("OperationPreferredDateUpdaterUseCase error: updatePreferredRunDate, error: %{public}@", log: .error, error.localizedDescription) + throw error } os_log("Updating preferredRunDate on operation with brokerId %{public}@ and profileQueryId %{public}@", log: .dataBrokerProtection, brokerId.description, profileQueryId.description) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationRetriesCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationRetriesCalculatorUseCase.swift index cca75f16b2..8035391f64 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationRetriesCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationRetriesCalculatorUseCase.swift @@ -20,14 +20,14 @@ import Foundation struct OperationRetriesCalculatorUseCase { - func calculateForScan(database: DataBrokerProtectionRepository, brokerId: Int64, profileQueryId: Int64) -> Int { - let events = database.fetchScanHistoryEvents(brokerId: brokerId, profileQueryId: profileQueryId) + func calculateForScan(database: DataBrokerProtectionRepository, brokerId: Int64, profileQueryId: Int64) throws -> Int { + let events = try database.fetchScanHistoryEvents(brokerId: brokerId, profileQueryId: profileQueryId) return events.filter { $0.type == .scanStarted }.count } - func calculateForOptOut(database: DataBrokerProtectionRepository, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) -> Int { - let events = database.fetchOptOutHistoryEvents(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) + func calculateForOptOut(database: DataBrokerProtectionRepository, brokerId: Int64, profileQueryId: Int64, extractedProfileId: Int64) throws -> Int { + let events = try database.fetchOptOutHistoryEvents(brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) return events.filter { $0.type == .optOutStarted }.count } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift index 9f278ab31a..d4d83f7b2a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift @@ -41,7 +41,14 @@ struct MismatchCalculatorUseCase { let pixelHandler: EventMapping func calculateMismatches() { - let brokerProfileQueryData = database.fetchAllBrokerProfileQueryData() + let brokerProfileQueryData: [BrokerProfileQueryData] + do { + brokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() + } catch { + os_log("MismatchCalculatorUseCase error: calculateMismatches, error: %{public}@", log: .error, error.localizedDescription) + return + } + let parentBrokerProfileQueryData = brokerProfileQueryData.filter { $0.dataBroker.parent == nil } for parent in parentBrokerProfileQueryData { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index 10d2bd799b..10bf5bb9e9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -65,7 +65,7 @@ final class ScanOperation: DataBrokerOperation { self.cookieHandler = cookieHandler } - func run(inputValue: Void, + func run(inputValue: InputValue, webViewHandler: WebViewHandler? = nil, actionsHandler: ActionsHandler? = nil, showWebView: Bool) async throws -> [ExtractedProfile] { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift index f00192e769..e825e0eb33 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift @@ -113,7 +113,7 @@ final class DataBrokerProtectionEngagementPixels { } func fireEngagementPixel(currentDate: Date = Date()) { - guard database.fetchProfile() != nil else { + guard (try? database.fetchProfile()) != nil else { os_log("No profile. We do not fire any pixel because we do not consider it an engaged user.", log: .dataBrokerProtection) return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift index db8d89de19..589ab33afb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift @@ -87,7 +87,14 @@ final class DataBrokerProtectionEventPixels { } private func fireWeeklyReportPixels() { - let data = database.fetchAllBrokerProfileQueryData() + let data: [BrokerProfileQueryData] + + do { + data = try database.fetchAllBrokerProfileQueryData() + } catch { + os_log("Database error: when attempting to fireWeeklyReportPixels, error: %{public}@", log: .error, error.localizedDescription) + return + } let dataInThePastWeek = data.filter(hadScanThisWeek(_:)) var newMatchesFoundInTheLastWeek = 0 diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index 1be9422b34..e3d0594950 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -26,6 +26,7 @@ enum ErrorCategory: Equatable { case validationError case clientError(httpCode: Int) case serverError(httpCode: Int) + case databaseError(domain: String, code: Int) case unclassified var toString: String { @@ -35,6 +36,7 @@ enum ErrorCategory: Equatable { case .unclassified: return "unclassified" case .clientError(let httpCode): return "client-error-\(httpCode)" case .serverError(let httpCode): return "server-error-\(httpCode)" + case .databaseError(let domain, let code): return "database-error-\(domain)-\(code)" } } } @@ -62,6 +64,9 @@ public enum DataBrokerProtectionPixels { } case error(error: DataBrokerProtectionError, dataBroker: String) + case generalError(error: Error, functionOccurredIn: String) + case secureVaultInitError(error: Error) + case secureVaultError(error: Error) case parentChildMatches(parent: String, child: String, value: Int) // Stage Pixels @@ -165,6 +170,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent { // Debug Pixels case .error: return "m_mac_data_broker_error" + case .generalError: return "m_mac_data_broker_error" + case .secureVaultInitError: return "m_mac_dbp_secure_vault_init_error" + case .secureVaultError: return "m_mac_dbp_secure_vault_error" case .backgroundAgentStarted: return "m_mac_dbp_background-agent_started" case .backgroundAgentStartedStoppingDueToAnotherInstanceRunning: return "m_mac_dbp_background-agent_started_stopping-due-to-another-instance-running" @@ -233,6 +241,8 @@ extension DataBrokerProtectionPixels: PixelKitEvent { } else { return ["dataBroker": dataBroker, "name": error.name] } + case .generalError(_, let functionOccurredIn): + return ["functionOccurredIn": functionOccurredIn] case .parentChildMatches(let parent, let child, let value): return ["parent": parent, "child": child, "value": String(value)] case .optOutStart(let dataBroker, let attemptId): @@ -302,8 +312,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .dailyActiveUser, .weeklyActiveUser, .monthlyActiveUser, + .scanningEventNewMatch, - .scanningEventReAppearance: + .scanningEventReAppearance, + + .secureVaultInitError, + .secureVaultError: return [:] case .ipcServerRegister, .ipcServerStartScheduler, @@ -334,6 +348,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Void)?) { } - func runQueuedOperations(showWebView: Bool, completion: ((Error?) -> Void)?) { } - func scanAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) { } + func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } + func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { } func runAllOperations(showWebView: Bool) { } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift index d6971d1a80..f10ca642e5 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift @@ -55,13 +55,14 @@ final class DataBrokerProtectionProcessor { } // MARK: - Public functions - func runAllScanOperations(showWebView: Bool = false, completion: (() -> Void)? = nil) { + func runAllScanOperations(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() runOperations(operationType: .scan, priorityDate: nil, - showWebView: showWebView) { + showWebView: showWebView) { errors in os_log("Scans done", log: .dataBrokerProtection) - completion?() + completion?(errors) self.calculateMisMatches() } } @@ -71,31 +72,34 @@ final class DataBrokerProtectionProcessor { mismatchUseCase.calculateMismatches() } - func runAllOptOutOperations(showWebView: Bool = false, completion: (() -> Void)? = nil) { + func runAllOptOutOperations(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { operationQueue.cancelAllOperations() runOperations(operationType: .optOut, priorityDate: nil, - showWebView: showWebView) { + showWebView: showWebView) { errors in os_log("Optouts done", log: .dataBrokerProtection) - completion?() + completion?(errors) } } - func runQueuedOperations(showWebView: Bool = false, completion: (() -> Void)? = nil ) { + func runQueuedOperations(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { runOperations(operationType: .all, priorityDate: Date(), - showWebView: showWebView) { + showWebView: showWebView) { errors in os_log("Queued operations done", log: .dataBrokerProtection) - completion?() + completion?(errors) } } - func runAllOperations(showWebView: Bool = false, completion: (() -> Void)? = nil ) { + func runAllOperations(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil ) { runOperations(operationType: .all, priorityDate: nil, - showWebView: showWebView) { + showWebView: showWebView) { errors in os_log("Queued operations done", log: .dataBrokerProtection) - completion?() + completion?(errors) } } @@ -107,11 +111,11 @@ final class DataBrokerProtectionProcessor { private func runOperations(operationType: DataBrokerOperationsCollection.OperationType, priorityDate: Date?, showWebView: Bool, - completion: @escaping () -> Void) { + completion: @escaping ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)) { // Before running new operations we check if there is any updates to the broker files. if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(errorReporter: nil) { - let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault) + let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) brokerUpdater.checkForUpdatesInBrokerJSONFiles() } @@ -120,18 +124,30 @@ final class DataBrokerProtectionProcessor { // This will try to fire the event weekly report pixels eventPixels.tryToFireWeeklyPixels() - let brokersProfileData = database.fetchAllBrokerProfileQueryData() - let dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, - operationType: operationType, - priorityDate: priorityDate, - showWebView: showWebView) + let dataBrokerOperationCollections: [DataBrokerOperationsCollection] - for collection in dataBrokerOperationCollections { - operationQueue.addOperation(collection) + do { + let brokersProfileData = try database.fetchAllBrokerProfileQueryData() + dataBrokerOperationCollections = createDataBrokerOperationCollections(from: brokersProfileData, + operationType: operationType, + priorityDate: priorityDate, + showWebView: showWebView) + + for collection in dataBrokerOperationCollections { + operationQueue.addOperation(collection) + } + } catch { + os_log("DataBrokerProtectionProcessor error: runOperations, error: %{public}@", log: .error, error.localizedDescription) + operationQueue.addBarrierBlock { + completion(DataBrokerProtectionSchedulerErrorCollection(oneTimeError: error)) + } + return } operationQueue.addBarrierBlock { - completion() + let operationErrors = dataBrokerOperationCollections.compactMap { $0.error } + let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionSchedulerErrorCollection(operationErrors: operationErrors) : nil + completion(errorCollection) } } @@ -157,6 +173,7 @@ final class DataBrokerProtectionProcessor { pixelHandler: pixelHandler, userNotificationService: userNotificationService, showWebView: showWebView) + collection.errorDelegate = self collections.append(collection) visitedDataBrokerIDs.insert(dataBrokerID) @@ -170,3 +187,22 @@ final class DataBrokerProtectionProcessor { os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) } } + +extension DataBrokerProtectionProcessor: DataBrokerOperationsCollectionErrorDelegate { + + func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, didErrorBeforeStartingBrokerOperations error: Error) { + + } + + func dataBrokerOperationsCollection(_ dataBrokerOperationsCollection: DataBrokerOperationsCollection, + didError error: Error, + whileRunningBrokerOperationData: BrokerOperationData, + withDataBrokerName dataBrokerName: String?) { + if let error = error as? DataBrokerProtectionError, + let dataBrokerName = dataBrokerName { + pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) + } else { + os_log("Cant handle error", log: .dataBrokerProtection) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 7ad31ed349..a6e5f15362 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -27,6 +27,23 @@ public enum DataBrokerProtectionSchedulerStatus: Codable { case running } +@objc +public class DataBrokerProtectionSchedulerErrorCollection: NSObject { + /* + This needs to be an NSObject (rather than a struct) so it can be represented in Objective C + for the IPC layer + */ + + public let oneTimeError: Error? + public let operationErrors: [Error]? + + public init(oneTimeError: Error? = nil, operationErrors: [Error]? = nil) { + self.oneTimeError = oneTimeError + self.operationErrors = operationErrors + super.init() + } +} + public protocol DataBrokerProtectionScheduler { var status: DataBrokerProtectionSchedulerStatus { get } @@ -35,9 +52,9 @@ public protocol DataBrokerProtectionScheduler { func startScheduler(showWebView: Bool) func stopScheduler() - func optOutAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) - func scanAllBrokers(showWebView: Bool, completion: ((Error?) -> Void)?) - func runQueuedOperations(showWebView: Bool, completion: ((Error?) -> Void)?) + func optOutAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + func scanAllBrokers(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) + func runQueuedOperations(showWebView: Bool, completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) func runAllOperations(showWebView: Bool) } @@ -138,7 +155,17 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch } self.status = .running os_log("Scheduler running...", log: .dataBrokerProtection) - self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] in + self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during startScheduler in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduler")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during startScheduler in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } self?.status = .idle completion(.finished) } @@ -154,40 +181,91 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch public func runAllOperations(showWebView: Bool = false) { os_log("Running all operations...", log: .dataBrokerProtection) - self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) + self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runAllOperations")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + } } - public func runQueuedOperations(showWebView: Bool = false, completion: ((Error?) -> Void)? = nil) { + public func runQueuedOperations(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { os_log("Running queued operations...", log: .dataBrokerProtection) dataBrokerProcessor.runQueuedOperations(showWebView: showWebView, - completion: { completion?(nil) }) + completion: { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runQueuedOperations")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + completion?(errors) + }) } - public func scanAllBrokers(showWebView: Bool = false, completion: ((Error?) -> Void)? = nil) { + public func scanAllBrokers(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)? = nil) { stopScheduler() userNotificationService.requestNotificationPermission() os_log("Scanning all brokers...", log: .dataBrokerProtection) - dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] in + dataBrokerProcessor.runAllScanOperations(showWebView: showWebView) { [weak self] errors in guard let self = self else { return } self.startScheduler(showWebView: showWebView) self.userNotificationService.sendFirstScanCompletedNotification() - if self.dataManager.hasMatches() { + if let hasMatches = try? self.dataManager.hasMatches(), + hasMatches { self.userNotificationService.scheduleCheckInNotificationIfPossible() } - completion?(nil) + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.scanAllBrokers")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.scanAllBrokers in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + + completion?(errors) } } - public func optOutAllBrokers(showWebView: Bool = false, completion: ((Error?) -> Void)?) { + public func optOutAllBrokers(showWebView: Bool = false, + completion: ((DataBrokerProtectionSchedulerErrorCollection?) -> Void)?) { os_log("Opting out all brokers...", log: .dataBrokerProtection) self.dataBrokerProcessor.runAllOptOutOperations(showWebView: showWebView, - completion: { completion?(nil) }) + completion: { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.optOutAllBrokers")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + } + + completion?(errors) + }) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 9f77b8a675..f99df1e614 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -23,9 +23,9 @@ import UserScript import Common protocol DBPUICommunicationDelegate: AnyObject { - func saveProfile() async -> Bool + func saveProfile() async throws func getUserProfile() -> DBPUIUserProfile? - func deleteProfileData() + func deleteProfileData() throws func addNameToCurrentUserProfile(_ name: DBPUIUserProfileName) -> Bool func setNameAtIndexInCurrentUserProfile(_ payload: DBPUINameAtIndex) -> Bool func removeNameAtIndexFromUserProfile(_ index: DBPUIIndex) -> Bool @@ -127,9 +127,13 @@ struct DBPUICommunicationLayer: Subfeature { func saveProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { os_log("Web UI requested to save the profile", log: .dataBrokerProtection) - let success = await delegate?.saveProfile() - - return DBPUIStandardResponse(version: Constants.version, success: success ?? false) + do { + try await delegate?.saveProfile() + return DBPUIStandardResponse(version: Constants.version, success: true) + } catch { + os_log("DBPUICommunicationLayer saveProfile, error: %{public}@", log: .error, error.localizedDescription) + return DBPUIStandardResponse(version: Constants.version, success: false) + } } func getCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { @@ -141,8 +145,13 @@ struct DBPUICommunicationLayer: Subfeature { } func deleteUserProfileData(params: Any, original: WKScriptMessage) async throws -> Encodable? { - delegate?.deleteProfileData() - return DBPUIStandardResponse(version: Constants.version, success: true) + do { + try delegate?.deleteProfileData() + return DBPUIStandardResponse(version: Constants.version, success: true) + } catch { + os_log("DBPUICommunicationLayer deleteUserProfileData, error: %{public}@", log: .error, error.localizedDescription) + return DBPUIStandardResponse(version: Constants.version, success: false) + } } func addNameToCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift index 688de0476e..4f26b33b8e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DataBrokerProtectionViewController.swift @@ -56,8 +56,10 @@ final public class DataBrokerProtectionViewController: NSViewController { prefs: prefs, webView: webView) + // Prepare the profile cache to avoid stuttering later + // It's in a task to avoid stuttering now Task { - _ = dataManager.fetchProfile(ignoresCache: true) + try? dataManager.prepareProfileCache() } super.init(nibName: nil, bundle: nil) diff --git a/LocalPackages/DataBrokerProtection/Tests/.swiftlint.yml b/LocalPackages/DataBrokerProtection/Tests/.swiftlint.yml new file mode 100644 index 0000000000..bf8a5655d9 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/.swiftlint.yml @@ -0,0 +1,17 @@ +disabled_rules: + - file_length + - unused_closure_parameter + - type_name + - force_cast + - force_try + - function_body_length + - cyclomatic_complexity + - identifier_name + - blanket_disable_command + - type_body_length + - explicit_non_final_class + - enforce_os_log_wrapper + +large_tuple: + warning: 6 + error: 10 diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift index 616e1a5fa9..0668f38d35 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProfileTests.swift @@ -162,7 +162,8 @@ final class DataBrokerProtectionProfileTests: XCTestCase { database: SecureStorageDatabaseProviderMock(), keystore: EmptySecureStorageKeyStoreProviderMock())) - let database = DataBrokerProtectionDatabase(vault: vault) + let database = DataBrokerProtectionDatabase(pixelHandler: MockDataBrokerProtectionPixelsHandler(), + vault: vault) let profile = DataBrokerProtectionProfile( names: [ @@ -175,7 +176,7 @@ final class DataBrokerProtectionProfileTests: XCTestCase { birthYear: 1980 ) - _=await database.save(profile) + _ = try! await database.save(profile) XCTAssertTrue(vault.wasSaveProfileQueryCalled) XCTAssertFalse(vault.wasUpdateProfileQueryCalled) XCTAssertFalse(vault.wasDeleteProfileQueryCalled) @@ -188,7 +189,8 @@ final class DataBrokerProtectionProfileTests: XCTestCase { database: SecureStorageDatabaseProviderMock(), keystore: EmptySecureStorageKeyStoreProviderMock())) - let database = DataBrokerProtectionDatabase(vault: vault) + let database = DataBrokerProtectionDatabase(pixelHandler: MockDataBrokerProtectionPixelsHandler(), + vault: vault) vault.brokers = [DataBroker.mock] vault.profileQueries = [ProfileQuery.mock] @@ -217,7 +219,7 @@ final class DataBrokerProtectionProfileTests: XCTestCase { birthYear: 1980 ) - _=await database.save(newProfile) + _ = try! await database.save(newProfile) XCTAssertTrue(vault.wasSaveProfileQueryCalled) XCTAssertTrue(vault.wasUpdateProfileQueryCalled) @@ -231,7 +233,8 @@ final class DataBrokerProtectionProfileTests: XCTestCase { database: SecureStorageDatabaseProviderMock(), keystore: EmptySecureStorageKeyStoreProviderMock())) - let database = DataBrokerProtectionDatabase(vault: vault) + let database = DataBrokerProtectionDatabase(pixelHandler: MockDataBrokerProtectionPixelsHandler(), + vault: vault) vault.brokers = [DataBroker.mock] vault.profileQueries = [ProfileQuery.mock] @@ -259,7 +262,7 @@ final class DataBrokerProtectionProfileTests: XCTestCase { birthYear: 1980 ) - _ = await database.save(newProfile) + _ = try! await database.save(newProfile) XCTAssertTrue(vault.wasSaveProfileQueryCalled) XCTAssertFalse(vault.wasUpdateProfileQueryCalled) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift index 8b899e12a1..7ba868976f 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStageDurationCalculatorTests.swift @@ -18,6 +18,7 @@ import BrowserServicesKit import Foundation +import SecureStorage import XCTest @testable import DataBrokerProtection @@ -120,6 +121,25 @@ final class DataBrokerProtectionStageDurationCalculatorTests: XCTestCase { } } + func testWhenErrorIsSecureVaultError_thenWeFireScanErorrPixelWithDatabaseErrorCategory() { + let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) + let error = SecureStorageError.encodingFailed + + sut.fireScanError(error: error) + + XCTAssertTrue(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count == 1) + + if let failurePixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last{ + switch failurePixel { + case .scanError(_, _, let category, _): + XCTAssertEqual(category, "database-error-SecureVaultError-13") + default: XCTFail("The scan error pixel should be fired") + } + } else { + XCTFail("A pixel should be fired") + } + } + func testWhenErrorIsNotDBPErrorAndNotURL_thenWeFireScanErrorPixelWithUnclassifiedErrorCategory() { let sut = DataBrokerProtectionStageDurationCalculator(dataBroker: "broker", handler: handler) let error = NSError(domain: NSCocoaErrorDomain, code: -1) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 0581735b88..dc8d0eda8c 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -706,10 +706,8 @@ final class MockDatabase: DataBrokerProtectionRepository { callsList.filter { $0 }.count > 0 // If one value is true. The database was called } - func save(_ profile: DataBrokerProtectionProfile) -> Bool { + func save(_ profile: DataBrokerProtectionProfile) throws { wasSaveProfileCalled = true - - return true } func fetchProfile() -> DataBrokerProtectionProfile? { diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift index 369f80ee32..c95e90e182 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/OperationPreferredDateUpdaterTests.swift @@ -46,7 +46,7 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { ) databaseMock.childBrokers = [childBroker] - sut.updateChildrenBrokerForParentBroker(.mock, profileQueryId: profileQueryId) + XCTAssertNoThrow(try sut.updateChildrenBrokerForParentBroker(.mock, profileQueryId: profileQueryId)) XCTAssertTrue(databaseMock.wasUpdatedPreferredRunDateForScanCalled) XCTAssertEqual(databaseMock.lastParentBrokerWhereChildSitesWhereFetched, "Test broker") @@ -57,7 +57,7 @@ final class OperationPreferredDateUpdaterTests: XCTestCase { func testWhenParentBrokerHasNoChildsites_thenNoCallsToTheDatabaseAreDone() { let sut = OperationPreferredDateUpdaterUseCase(database: databaseMock) - sut.updateChildrenBrokerForParentBroker(.mock, profileQueryId: 1) + XCTAssertNoThrow(try sut.updateChildrenBrokerForParentBroker(.mock, profileQueryId: 1)) XCTAssertFalse(databaseMock.wasDatabaseCalled) } From 773d073bf9734c7c8389a7bbc6ebadab563b5faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 9 Apr 2024 15:23:46 +0200 Subject: [PATCH 041/221] Keep windows miniaturized upon relaunching (#2569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/276630244458377/1206774148393444/f Tech Design URL: CC: **Description**: Stores `isMiniaturized` property in `WindowRestorationItem` and applies to respective window during launch. **Steps to test this PR**: 1. Open a bunch of windows (preferably with a loaded URL). 2. Minimize some of them. 3. Restart the app. 4. Previously miniaturized windows should reopen in the same state (visible only in dock). — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../StateRestoration/WindowManager+StateRestoration.swift | 7 ++++++- DuckDuckGo/Windows/View/WindowsManager.swift | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift b/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift index f9c84e5055..aaa38da89c 100644 --- a/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift +++ b/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift @@ -52,7 +52,7 @@ extension WindowsManager { } private class func setUpWindow(from item: WindowRestorationItem) { - guard let window = openNewWindow(with: item.model, showWindow: true) else { return } + guard let window = openNewWindow(with: item.model, showWindow: !item.isMiniaturized, isMiniaturized: item.isMiniaturized) else { return } window.setContentSize(item.frame.size) window.setFrameOrigin(item.frame.origin) } @@ -135,11 +135,13 @@ final class WindowRestorationItem: NSObject, NSSecureCoding { private enum NSSecureCodingKeys { static let frame = "frame" static let model = "model" + static let isMiniaturized = "isMiniaturized" } let model: TabCollectionViewModel let frame: NSRect + let isMiniaturized: Bool @MainActor init?(windowController: MainWindowController) { @@ -150,6 +152,7 @@ final class WindowRestorationItem: NSObject, NSSecureCoding { self.frame = windowController.window!.frame self.model = windowController.mainViewController.tabCollectionViewModel + self.isMiniaturized = windowController.window!.isMiniaturized } static var supportsSecureCoding: Bool { true } @@ -161,10 +164,12 @@ final class WindowRestorationItem: NSObject, NSSecureCoding { } self.model = model self.frame = coder.decodeRect(forKey: NSSecureCodingKeys.frame) + self.isMiniaturized = coder.decodeBool(forKey: NSSecureCodingKeys.isMiniaturized) } func encode(with coder: NSCoder) { coder.encode(frame, forKey: NSSecureCodingKeys.frame) coder.encode(model, forKey: NSSecureCodingKeys.model) + coder.encode(isMiniaturized, forKey: NSSecureCodingKeys.isMiniaturized) } } diff --git a/DuckDuckGo/Windows/View/WindowsManager.swift b/DuckDuckGo/Windows/View/WindowsManager.swift index 245b36ed0c..007f850990 100644 --- a/DuckDuckGo/Windows/View/WindowsManager.swift +++ b/DuckDuckGo/Windows/View/WindowsManager.swift @@ -61,7 +61,8 @@ final class WindowsManager { contentSize: NSSize? = nil, showWindow: Bool = true, popUp: Bool = false, - lazyLoadTabs: Bool = false) -> MainWindow? { + lazyLoadTabs: Bool = false, + isMiniaturized: Bool = false) -> MainWindow? { let mainWindowController = makeNewWindow(tabCollectionViewModel: tabCollectionViewModel, popUp: popUp, burnerMode: burnerMode, @@ -71,6 +72,8 @@ final class WindowsManager { mainWindowController.window?.setContentSize(contentSize) } + mainWindowController.window?.setIsMiniaturized(isMiniaturized) + if let droppingPoint { mainWindowController.window?.setFrameOrigin(droppingPoint: droppingPoint) From 5be1c5af53c9b01a27099170c08d82fdd016f04b Mon Sep 17 00:00:00 2001 From: Halle <378795+Halle@users.noreply.github.com> Date: Tue, 9 Apr 2024 06:25:10 -0700 Subject: [PATCH 042/221] Adds a series of UI tests for Bookmarks and Favorites Task/Issue URL: https://app.asana.com/0/1199230911884351/1205717021705367/f Tech Design URL: CC: **Description**: Adds a series of UI tests for Bookmarks and Favorites **Steps to test this PR**: 1. Open the scheme **UI Tests** 2. Navigate to the test pane 3. Run BookmarksAndFavoritesTests **Note**: If this builds a `review` build that is treated as a first run on your system during every test (for instance, asking you where to put the application), please **build and run** the scheme once instead of running its tests, and go through the new app install first run steps once before running the tests, and answer any first install questions. **UI Tests general guidelines for everyone**: it unfortunately isn't possible to multitask while running UI tests on your local machine, because you will change or intercept screen interactions that the tests depend on. If you experience failures in your local environment, please always send the `xcresult` bundle so it is possible to view what happened differently on your machine, even if it seems like the same failure as a previous failure (it may be subtly different). Since the `xcresult` bundle will include a screen recording, for your privacy, please consider things like your chats, calls, and open documents that you don't want to send in a screen recording when you are running the tests. Thank you very much for your help! --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../Bookmarks/Services/ContextualMenu.swift | 5 + ...kmarkManagementSidebarViewController.swift | 1 + .../View/BookmarkOutlineCellView.swift | 1 + .../View/BookmarkTableCellView.swift | 4 + .../Extensions/NSMenuItemExtension.swift | 5 + DuckDuckGo/HomePage/View/FavoritesView.swift | 16 +- DuckDuckGo/Menus/MainMenu.swift | 3 +- .../AddressBarButtonsViewController.swift | 7 +- .../NavigationBar/View/MoreOptionsMenu.swift | 3 +- .../View/PreferencesAppearanceView.swift | 2 +- .../TabExtensions/ContextMenuManager.swift | 2 +- UITests/AutocompleteTests.swift | 1 - UITests/BookmarksAndFavoritesTests.swift | 724 ++++++++++++++++++ UITests/BookmarksBarTests.swift | 1 - UITests/Common/UITests.swift | 2 +- UITests/Common/XCUIElementExtension.swift | 16 + 17 files changed, 780 insertions(+), 17 deletions(-) create mode 100644 UITests/BookmarksAndFavoritesTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2bbc99a10b..dfa32b6122 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3291,6 +3291,7 @@ EE339228291BDEFD009F62C1 /* JSAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE339227291BDEFD009F62C1 /* JSAlertController.swift */; }; EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */; }; EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; @@ -4772,6 +4773,7 @@ EE0429DF2BA31D2F009EB20F /* FindInPageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindInPageTests.swift; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; + EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksAndFavoritesTests.swift; sourceTree = ""; }; EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarTests.swift; sourceTree = ""; }; @@ -6556,6 +6558,7 @@ children = ( EEBCE6802BA444FA00B9DF00 /* Common */, EED735352BB46B6000F173D6 /* AutocompleteTests.swift */, + EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */, EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */, EE02D41B2BB460A600DBE6B3 /* BrowsingHistoryTests.swift */, EE0429DF2BA31D2F009EB20F /* FindInPageTests.swift */, @@ -12279,6 +12282,7 @@ EE02D41A2BB4609900DBE6B3 /* UITests.swift in Sources */, EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, EE02D4212BB460FE00DBE6B3 /* StringExtension.swift in Sources */, + EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */, EE02D4222BB4611A00DBE6B3 /* TestsURLExtension.swift in Sources */, 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */, EEBCE6832BA463DD00B9DF00 /* NSImageExtensions.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index f79e265d96..eb77fd956f 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -163,11 +163,15 @@ private extension ContextualMenu { static func addBookmarkToFavoritesMenuItem(isFavorite: Bool, bookmark: Bookmark?) -> NSMenuItem { let title = isFavorite ? UserText.removeFromFavorites : UserText.addToFavorites return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) + .withAccessibilityIdentifier(isFavorite == false ? "ContextualMenu.addBookmarkToFavoritesMenuItem" : + "ContextualMenu.removeBookmarkFromFavoritesMenuItem") } static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { let title = allFavorites ? UserText.removeFromFavorites : UserText.addToFavorites + let accessibilityValue = allFavorites ? "Favorited" : "Unfavorited" return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmarks) + .withAccessibilityIdentifier("ContextualMenu.addBookmarksToFavoritesMenuItem").withAccessibilityValue(accessibilityValue) } static func editBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { @@ -180,6 +184,7 @@ private extension ContextualMenu { static func deleteBookmarkMenuItem(bookmark: Bookmark?) -> NSMenuItem { menuItem(UserText.bookmarksBarContextMenuDelete, #selector(BookmarkMenuItemSelectors.deleteBookmark(_:)), bookmark) + .withAccessibilityIdentifier("ContextualMenu.deleteBookmark") } static func moveToEndMenuItem(entity: BaseBookmarkEntity?, parent: BookmarkFolder?) -> NSMenuItem { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index 53e502383b..f567a9f124 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -89,6 +89,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { tabSwitcherButton.menu = NSMenu { for content in Tab.TabContent.displayableTabTypes { NSMenuItem(title: content.title!, representedObject: content) + .withAccessibilityIdentifier("BookmarkManagementSidebarViewController.\(content.title!)") } } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index 603849bbbf..06b868c826 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -82,6 +82,7 @@ final class BookmarkOutlineCellView: NSTableCellView { faviconImageView.imageScaling = .scaleProportionallyDown faviconImageView.wantsLayer = true faviconImageView.layer?.cornerRadius = 2.0 + faviconImageView.setAccessibilityIdentifier("BookmarkOutlineCellView.favIconImageView") titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.isEditable = false diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index 8bfdc3c4fb..3a3d3ea0d9 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -129,6 +129,7 @@ final class BookmarkTableCellView: NSTableCellView { menuButton.translatesAutoresizingMaskIntoConstraints = false menuButton.isBordered = false menuButton.isHidden = true + menuButton.setAccessibilityIdentifier("BookmarkTableCellView.menuButton") } private func setupLayout() { @@ -209,11 +210,14 @@ final class BookmarkTableCellView: NSTableCellView { faviconImageView.image = bookmark.favicon(.small) ?? .bookmarkDefaultFavicon + faviconImageView.setAccessibilityIdentifier("BookmarkTableCellView.favIconImageView") if bookmark.isFavorite { accessoryImageView.isHidden = false } accessoryImageView.image = bookmark.isFavorite ? .favoriteFilledBorder : nil + accessoryImageView.setAccessibilityIdentifier("BookmarkTableCellView.accessoryImageView") + accessoryImageView.setAccessibilityValue(bookmark.isFavorite ? "Favorited" : "Unfavorited") titleLabel.stringValue = bookmark.title primaryTitleLabelValue = bookmark.title tertiaryTitleLabelValue = bookmark.url diff --git a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift index b3f0870252..dfe76b5092 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift @@ -110,6 +110,11 @@ extension NSMenuItem { return self } + func withAccessibilityValue(_ accessibilityValue: String) -> NSMenuItem { + self.setAccessibilityValue(accessibilityValue) + return self + } + @discardableResult func withImage(_ image: NSImage?) -> NSMenuItem { self.image = image diff --git a/DuckDuckGo/HomePage/View/FavoritesView.swift b/DuckDuckGo/HomePage/View/FavoritesView.swift index 52e7f585d1..39f74b58cf 100644 --- a/DuckDuckGo/HomePage/View/FavoritesView.swift +++ b/DuckDuckGo/HomePage/View/FavoritesView.swift @@ -324,12 +324,12 @@ struct Favorite: View { .link { model.open(bookmark) }.contextMenu(ContextMenu(menuItems: { - Button(UserText.openInNewTab, action: { model.openInNewTab(bookmark) }) - Button(UserText.openInNewWindow, action: { model.openInNewWindow(bookmark) }) + Button(UserText.openInNewTab, action: { model.openInNewTab(bookmark) }).accessibilityIdentifier("HomePage.Views.openInNewTab") + Button(UserText.openInNewWindow, action: { model.openInNewWindow(bookmark) }).accessibilityIdentifier("HomePage.Views.openInNewWindow") Divider() - Button(UserText.edit, action: { model.edit(bookmark) }) - Button(UserText.removeFavorite, action: { model.removeFavorite(bookmark) }) - Button(UserText.deleteBookmark, action: { model.deleteBookmark(bookmark) }) + Button(UserText.edit, action: { model.edit(bookmark) }).accessibilityIdentifier("HomePage.Views.editBookmark") + Button(UserText.removeFavorite, action: { model.removeFavorite(bookmark) }).accessibilityIdentifier("HomePage.Views.removeFavorite") + Button(UserText.deleteBookmark, action: { model.deleteBookmark(bookmark) }).accessibilityIdentifier("HomePage.Views.deleteBookmark") })) } @@ -344,13 +344,13 @@ extension HomePage.Models.FavoriteModel { var favoriteView: some View { switch favoriteType { case .bookmark(let bookmark): - HomePage.Views.Favorite(bookmark: bookmark) + HomePage.Views.Favorite(bookmark: bookmark)?.accessibilityIdentifier("HomePage.Models.FavoriteModel.\(bookmark.title)") case .addButton: - HomePage.Views.FavoritesGridAddButton() + HomePage.Views.FavoritesGridAddButton().accessibilityIdentifier("HomePage.Models.FavoriteModel.addButton") case .ghostButton: - HomePage.Views.FavoritesGridGhostButton() + HomePage.Views.FavoritesGridGhostButton().accessibilityIdentifier("HomePage.Models.FavoriteModel.ghostButton") } } } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 31d25d3d31..d5bdd2d495 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -69,7 +69,7 @@ import SubscriptionUI var forwardMenuItem: NSMenuItem { historyMenu.forwardMenuItem } // MARK: Bookmarks - let manageBookmarksMenuItem = NSMenuItem(title: UserText.mainMenuHistoryManageBookmarks, action: #selector(MainViewController.showManageBookmarks)) + let manageBookmarksMenuItem = NSMenuItem(title: UserText.mainMenuHistoryManageBookmarks, action: #selector(MainViewController.showManageBookmarks)).withAccessibilityIdentifier("MainMenu.manageBookmarksMenuItem") var bookmarksMenuToggleBookmarksBarMenuItem = NSMenuItem(title: "BookmarksBarMenuPlaceholder", action: #selector(MainViewController.toggleBookmarksBarFromMenu), keyEquivalent: "B") let importBookmarksMenuItem = NSMenuItem(title: UserText.importBookmarks, action: #selector(AppDelegate.openImportBrowserDataWindow)) let bookmarksMenu = NSMenu(title: UserText.bookmarks) @@ -306,6 +306,7 @@ import SubscriptionUI .submenu(favoritesMenu.buildItems { NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage)) .withImage(.favorite) + .withAccessibilityIdentifier("MainMenu.favoriteThisPage") NSMenuItem.separator() }) .withImage(.favorite) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 7afc044118..7bb06c097f 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -272,7 +272,7 @@ final class AddressBarButtonsViewController: NSViewController { private func updateBookmarkButtonVisibility() { guard view.window?.isPopUpWindow == false else { return } - bookmarkButton.setAccessibilityIdentifier("Bookmarks Button") + bookmarkButton.setAccessibilityIdentifier("AddressBarButtonsViewController.bookmarkButton") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true var showBookmarkButton: Bool { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } @@ -727,15 +727,18 @@ final class AddressBarButtonsViewController: NSViewController { private func updateBookmarkButtonImage(isUrlBookmarked: Bool = false) { if let url = tabViewModel?.tab.content.url, - isUrlBookmarked || bookmarkManager.isUrlBookmarked(url: url) { + isUrlBookmarked || bookmarkManager.isUrlBookmarked(url: url) + { bookmarkButton.image = .bookmarkFilled bookmarkButton.mouseOverTintColor = NSColor.bookmarkFilledTint bookmarkButton.toolTip = UserText.editBookmarkTooltip + bookmarkButton.setAccessibilityValue("Bookmarked") } else { bookmarkButton.mouseOverTintColor = nil bookmarkButton.image = .bookmark bookmarkButton.contentTintColor = nil bookmarkButton.toolTip = UserText.addBookmarkTooltip + bookmarkButton.setAccessibilityValue("Unbookmarked") } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index ed8a2e2b76..e784fccf14 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -270,7 +270,7 @@ final class MoreOptionsMenu: NSMenu { .targetting(self) .withImage(.bookmarks) .withSubmenu(bookmarksSubMenu) - + .withAccessibilityIdentifier("MoreOptionsMenu.openBookmarks") addItem(withTitle: UserText.downloads, action: #selector(openDownloads), keyEquivalent: "j") .targetting(self) .withImage(.downloads) @@ -641,6 +641,7 @@ final class BookmarksSubMenu: NSMenu { let bookmarkPageItem = addItem(withTitle: UserText.bookmarkThisPage, action: #selector(MoreOptionsMenu.bookmarkPage(_:)), keyEquivalent: "d") .withModifierMask([.command]) .targetting(target) + .withAccessibilityIdentifier("MoreOptionsMenu.bookmarkPage") bookmarkPageItem.isEnabled = tabCollectionViewModel.selectedTabViewModel?.canBeBookmarked == true diff --git a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift index a7b30dae90..c44ff7c3bd 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift @@ -106,7 +106,7 @@ extension Preferences { if model.isContinueSetUpAvailable { ToggleMenuItem(UserText.newTabSetUpSectionTitle, isOn: $model.isContinueSetUpVisible) } - ToggleMenuItem(UserText.newTabFavoriteSectionTitle, isOn: $model.isFavoriteVisible) + ToggleMenuItem(UserText.newTabFavoriteSectionTitle, isOn: $model.isFavoriteVisible).accessibilityIdentifier("Preferences.AppearanceView.showFavoritesToggle") ToggleMenuItem(UserText.newTabRecentActivitySectionTitle, isOn: $model.isRecentActivityVisible) } diff --git a/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift b/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift index 787ac89300..e966c8b7a0 100644 --- a/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift +++ b/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift @@ -234,7 +234,7 @@ private extension ContextMenuManager { } func bookmarkPageMenuItem() -> NSMenuItem { - NSMenuItem(title: UserText.bookmarkPage, action: #selector(MainViewController.bookmarkThisPage), target: nil, keyEquivalent: "") + NSMenuItem(title: UserText.bookmarkPage, action: #selector(MainViewController.bookmarkThisPage), target: nil, keyEquivalent: "").withAccessibilityIdentifier("ContextMenuManager.bookmarkPageMenuItem") } func openLinkInNewWindowMenuItem(from item: NSMenuItem) -> NSMenuItem { diff --git a/UITests/AutocompleteTests.swift b/UITests/AutocompleteTests.swift index 2ef08b7e8c..4c730f0f80 100644 --- a/UITests/AutocompleteTests.swift +++ b/UITests/AutocompleteTests.swift @@ -16,7 +16,6 @@ // limitations under the License. // -import Common import XCTest class AutocompleteTests: XCTestCase { diff --git a/UITests/BookmarksAndFavoritesTests.swift b/UITests/BookmarksAndFavoritesTests.swift new file mode 100644 index 0000000000..bf06b4aac6 --- /dev/null +++ b/UITests/BookmarksAndFavoritesTests.swift @@ -0,0 +1,724 @@ +// +// BookmarksAndFavoritesTests.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 XCTest + +class BookmarksAndFavoritesTests: XCTestCase { + private var app: XCUIApplication! + private var pageTitle: String! + private var urlForBookmarksBar: URL! + private let titleStringLength = 12 + + private var addressBarBookmarkButton: XCUIElement! + private var addressBarTextField: XCUIElement! + private var bookmarkDialogBookmarkFolderDropdown: XCUIElement! + private var bookmarkPageContextMenuItem: XCUIElement! + private var bookmarkPageMenuItem: XCUIElement! + private var bookmarksBarCollectionView: XCUIElement! + private var bookmarksDialogAddToFavoritesCheckbox: XCUIElement! + private var bookmarksManagementAccessoryImageView: XCUIElement! + private var bookmarksMenu: XCUIElement! + private var bookmarksTabPopup: XCUIElement! + private var bookmarkTableCellViewFavIconImageView: XCUIElement! + private var bookmarkTableCellViewMenuButton: XCUIElement! + private var contextualMenuAddBookmarkToFavoritesMenuItem: XCUIElement! + private var contextualMenuDeleteBookmarkMenuItem: XCUIElement! + private var contextualMenuRemoveBookmarkFromFavoritesMenuItem: XCUIElement! + private var defaultBookmarkDialogButton: XCUIElement! + private var defaultBookmarkOtherButton: XCUIElement! + private var favoriteGridAddFavoriteButton: XCUIElement! + private var favoriteThisPageMenuItem: XCUIElement! + private var manageBookmarksMenuItem: XCUIElement! + private var openBookmarksMenuItem: XCUIElement! + private var optionsButton: XCUIElement! + private var removeFavoritesContextMenuItem: XCUIElement! + private var resetBookMarksMenuItem: XCUIElement! + private var settingsAppearanceButton: XCUIElement! + private var showBookmarksBarPreferenceToggle: XCUIElement! + private var showBookmarksBarAlways: XCUIElement! + private var showBookmarksBarPopup: XCUIElement! + private var showFavoritesPreferenceToggle: XCUIElement! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" + pageTitle = UITests.randomPageTitle(length: titleStringLength) + urlForBookmarksBar = UITests.simpleServedPage(titled: pageTitle) + addressBarBookmarkButton = app.buttons["AddressBarButtonsViewController.bookmarkButton"] + addressBarTextField = app.windows.textFields["AddressBarViewController.addressBarTextField"] + bookmarkDialogBookmarkFolderDropdown = app.popUpButtons["bookmark.add.folder.dropdown"] + bookmarkPageContextMenuItem = app.menuItems["ContextMenuManager.bookmarkPageMenuItem"] + bookmarkPageMenuItem = app.menuItems["MoreOptionsMenu.bookmarkPage"] + bookmarksBarCollectionView = app.collectionViews["BookmarksBarViewController.bookmarksBarCollectionView"] + bookmarksDialogAddToFavoritesCheckbox = app.checkBoxes["bookmark.add.add.to.favorites.button"] + bookmarksManagementAccessoryImageView = app.images["BookmarkTableCellView.accessoryImageView"] + bookmarksMenu = app.menuBarItems["Bookmarks"] + bookmarksTabPopup = app.popUpButtons["Bookmarks"] + bookmarkTableCellViewFavIconImageView = app.images["BookmarkTableCellView.favIconImageView"] + bookmarkTableCellViewMenuButton = app.buttons["BookmarkTableCellView.menuButton"] + contextualMenuAddBookmarkToFavoritesMenuItem = app.menuItems["ContextualMenu.addBookmarkToFavoritesMenuItem"] + contextualMenuDeleteBookmarkMenuItem = app.menuItems["ContextualMenu.deleteBookmark"] + contextualMenuRemoveBookmarkFromFavoritesMenuItem = app.menuItems["ContextualMenu.removeBookmarkFromFavoritesMenuItem"] + defaultBookmarkDialogButton = app.buttons["BookmarkDialogButtonsView.defaultButton"] + defaultBookmarkOtherButton = app.buttons["BookmarkDialogButtonsView.otherButton"] + favoriteGridAddFavoriteButton = app.staticTexts["HomePage.Models.FavoriteModel.addButton"] + favoriteThisPageMenuItem = app.menuItems["MainMenu.favoriteThisPage"] + manageBookmarksMenuItem = app.menuItems["MainMenu.manageBookmarksMenuItem"] + openBookmarksMenuItem = app.menuItems["MoreOptionsMenu.openBookmarks"] + optionsButton = app.buttons["NavigationBarViewController.optionsButton"] + removeFavoritesContextMenuItem = app.menuItems["HomePage.Views.removeFavorite"] + resetBookMarksMenuItem = app.menuItems["MainMenu.resetBookmarks"] + settingsAppearanceButton = app.buttons["PreferencesSidebar.appearanceButton"] + showBookmarksBarAlways = app.menuItems["Preferences.AppearanceView.showBookmarksBarAlways"] + showBookmarksBarPopup = app.popUpButtons["Preferences.AppearanceView.showBookmarksBarPopUp"] + showBookmarksBarPreferenceToggle = app.checkBoxes["Preferences.AppearanceView.showBookmarksBarPreferenceToggle"] + showFavoritesPreferenceToggle = app.checkBoxes["Preferences.AppearanceView.showFavoritesToggle"] + + app.launch() + resetBookmarks() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Let's enforce a single window + app.typeKey("n", modifierFlags: .command) + } + + func test_bookmarks_canBeAddedTo_withContextClickBookmarkThisPage() { + openSiteToBookmark(bookmarkingViaDialog: false, escapingDialog: false) + app.windows.webViews[pageTitle].rightClick() + bookmarkPageContextMenuItem.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // Check Add Bookmark dialog for existence but don't click on it + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog option button didn't appear with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + addressBarBookmarkButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The address bar bookmark button didn't appear with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + bookmarkDialogBookmarkFolderDropdown.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog's bookmark folder dropdown didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarkDialogBookmarkFolderDropdownValue = try? XCTUnwrap( // Bookmark dialog must default to "Bookmarks" folder + bookmarkDialogBookmarkFolderDropdown.value as? String, + "It wasn't possible to get the value of the \"Add bookmark\" dialog's bookmark folder dropdown as String" + ) + XCTAssertEqual( + bookmarkDialogBookmarkFolderDropdownValue, + "Bookmarks", + "The accessibility value of the \"Add bookmark\" dialog's bookmark folder dropdown must be \"Bookmarks\"." + ) + let addressBarBookmarkButtonValue = try? XCTUnwrap( + addressBarBookmarkButton.value as? String, + "It wasn't possible to get the value of the address bar bookmark button as String" + ) + + XCTAssertEqual( // The bookmark icon is already in a filled state and it isn't necessary to click the add button + addressBarBookmarkButtonValue, + "Bookmarked", + "The accessibility value of the address bar bookmark button must be \"Bookmarked\", which indicates the icon in the filled state." + ) + + bookmarksMenu.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // And the bookmark is found in the Bookmarks menu + app.menuItems[pageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmark in the \"Bookmarks\" menu with the title of the test page didn't appear with the expected title in a reasonable timeframe." + ) + } + + func test_bookmarks_canBeAddedTo_withSettingsItemBookmarkThisPage() { + openSiteToBookmark(bookmarkingViaDialog: false, escapingDialog: false) + optionsButton.clickAfterExistenceTestSucceeds() + openBookmarksMenuItem.hoverAfterExistenceTestSucceeds() + bookmarkPageMenuItem.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // Check Add Bookmark dialog for existence but don't click on it + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog option button didn't appear with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + addressBarBookmarkButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The address bar bookmark button didn't appear with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + bookmarkDialogBookmarkFolderDropdown.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog's bookmark folder dropdown didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarkDialogBookmarkFolderDropdownValue = try? XCTUnwrap( + bookmarkDialogBookmarkFolderDropdown.value as? String, + "It wasn't possible to get the value of the \"Add bookmark\" dialog's bookmark folder dropdown as String" + ) + XCTAssertEqual( // Bookmark dialog must default to "Bookmarks" folder + bookmarkDialogBookmarkFolderDropdownValue, + "Bookmarks", + "The accessibility value of the \"Add bookmark\" dialog's bookmark folder dropdown must be \"Bookmarks\"." + ) + + let addressBarBookmarkButtonValue = try? XCTUnwrap( + addressBarBookmarkButton.value as? String, + "It wasn't possible to get the value of the address bar bookmark button as String" + ) + XCTAssertEqual( // The bookmark icon is already in a filled state and it isn't necessary to click the add button + addressBarBookmarkButtonValue, + "Bookmarked", + "The accessibility value of the address bar bookmark button must be \"Bookmarked\", which indicates the icon in the filled state." + ) + + bookmarksMenu.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // And the bookmark is found in the Bookmarks menu + app.menuItems[pageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmark in the \"Bookmarks\" menu with the title of the test page didn't appear with the expected title in a reasonable timeframe." + ) + } + + func test_bookmarks_canBeAddedTo_byClickingBookmarksButtonInAddressBar() { + openSiteToBookmark(bookmarkingViaDialog: false, escapingDialog: false) + // In order to directly click the bookmark button in the address bar, we need to hover over something in the bar area + optionsButton.hoverAfterExistenceTestSucceeds() + addressBarBookmarkButton.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // Check Add Bookmark dialog for existence but don't click on it + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog option button didn't appear with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + bookmarkDialogBookmarkFolderDropdown.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog's bookmark folder dropdown didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarkDialogBookmarkFolderDropdownValue = try? XCTUnwrap( + bookmarkDialogBookmarkFolderDropdown.value as? String, + "It wasn't possible to get the value of the \"Add bookmark\" dialog's bookmark folder dropdown as String" + ) + + XCTAssertEqual( // Bookmark dialog must default to "Bookmarks" folder + bookmarkDialogBookmarkFolderDropdownValue, + "Bookmarks", + "The accessibility value of the \"Add bookmark\" dialog's bookmark folder dropdown must be \"Bookmarks\"." + ) + let addressBarBookmarkButtonValue = try? XCTUnwrap( + addressBarBookmarkButton.value as? String, + "It wasn't possible to get the value of the address bar bookmark button as String" + ) + XCTAssertEqual( // The bookmark icon is already in a filled state and it isn't necessary to click the add button + addressBarBookmarkButtonValue, + "Bookmarked", + "The accessibility value of the address bar bookmark button must be \"Bookmarked\", which indicates the icon in the filled state." + ) + + bookmarksMenu.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // And the bookmark is found in the Bookmarks menu + app.menuItems[pageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmark in the \"Bookmarks\" menu with the title of the test page didn't appear with the expected title in a reasonable timeframe." + ) + } + + func test_favorites_canBeAddedTo_byClickingFavoriteThisPageMenuBarItem() { + openSiteToBookmark(bookmarkingViaDialog: false, escapingDialog: false) + bookmarksMenu.clickAfterExistenceTestSucceeds() + favoriteThisPageMenuItem.clickAfterExistenceTestSucceeds() + + XCTAssertTrue( // Check Add Bookmark dialog for existence but don't click on it + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog option button didn't appear with the expected title in a reasonable timeframe." + ) + let addressBarBookmarkButtonValue = try? XCTUnwrap( + addressBarBookmarkButton.value as? String, + "It wasn't possible to get the value of the address bar bookmark button as String" + ) + XCTAssertEqual( // The bookmark icon is already in a filled state and it isn't necessary to click the add button + addressBarBookmarkButtonValue, + "Bookmarked", + "The accessibility value of the address bar bookmark button must be \"Bookmarked\", which indicates the icon in the filled state." + ) + XCTAssertTrue( // Check Add Bookmark dialog for existence but don't click on it + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog option button didn't appear with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + XCTAssertEqual( // The favorite checkbox in the dialog is already checked + bookmarksDialogAddToFavoritesCheckboxValue, + true, + "The the value of the bookmarks dialog's add to favorites checkbox must be checked, which indicates that the item has been favorited." + ) + } + + func test_favorites_canBeAddedTo_byClickingAddFavoriteInAddBookmarkPopover() { + openSiteToBookmark(bookmarkingViaDialog: false, escapingDialog: false) + // In order to directly click the bookmark button in the address bar, we need to hover over something in the bar area + optionsButton.hoverAfterExistenceTestSucceeds() + + addressBarBookmarkButton.clickAfterExistenceTestSucceeds() + XCTAssertTrue( // Check Add Bookmark dialog for existence before adding to favorites + defaultBookmarkDialogButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Add bookmark\" dialog option button didn't appear with the expected title in a reasonable timeframe." + ) + + bookmarksDialogAddToFavoritesCheckbox.clickAfterExistenceTestSucceeds() + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + XCTAssertEqual( // The favorite checkbox in the dialog is already checked + bookmarksDialogAddToFavoritesCheckboxValue, + true, + "The the value of the bookmarks dialog's add to favorites checkbox must be checked, which indicates that the item has been favorited." + ) + } + + func test_favorites_canBeManuallyAddedTo_byClickingAddFavoriteInNewTabPage() throws { + toggleBookmarksBarShowFavoritesOn() + + favoriteGridAddFavoriteButton.clickAfterExistenceTestSucceeds() + let pageTitleForAddFavoriteDialog: String = try XCTUnwrap(pageTitle, "Couldn't unwrap page title") + let urlForAddFavoriteDialog = try XCTUnwrap(urlForBookmarksBar, "Couldn't unwrap page url") + app.typeText("\(pageTitleForAddFavoriteDialog)\t") + app.typeURL(urlForAddFavoriteDialog) + let newFavorite = app.otherElements.staticTexts[pageTitleForAddFavoriteDialog] + + XCTAssertTrue( + newFavorite.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The new favorite on the new tab page did not become available in a reasonable timeframe." + ) + } + + func test_favorites_canBeAddedToFromManageBookmarksView() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + bookmarksMenu.clickAfterExistenceTestSucceeds() + manageBookmarksMenuItem.clickAfterExistenceTestSucceeds() + bookmarkTableCellViewFavIconImageView.hoverAfterExistenceTestSucceeds() + bookmarkTableCellViewMenuButton.clickAfterExistenceTestSucceeds() + + contextualMenuAddBookmarkToFavoritesMenuItem.clickAfterExistenceTestSucceeds() + XCTAssertTrue( + bookmarksManagementAccessoryImageView.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmarks accessory view favorites indicator didn't load with the expected title in a reasonable timeframe." + ) + let bookmarksManagementAccessoryImageViewValue = try? XCTUnwrap( + bookmarksManagementAccessoryImageView.value as? String, + "It wasn't possible to get the value of the bookmarks management accessory image view as String" + ) + + XCTAssertEqual( + bookmarksManagementAccessoryImageViewValue, + "Favorited", + "The accessibility value of the favorite accessory view on the bookmark management view must be \"Favorited\"." + ) + } + + func test_bookmarks_canBeViewedInBookmarkMenuItem() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + addressBarBookmarkButton.clickAfterExistenceTestSucceeds() + + bookmarksMenu.clickAfterExistenceTestSucceeds() + let bookmarkedItemInMenu = app.menuItems[pageTitle] + + XCTAssertTrue( + bookmarkedItemInMenu.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmarked page couldn't be detected in the bookmarks menu in a reasonable timeframe." + ) + } + + func test_bookmarks_canBeViewedInAddressBarBookmarkDialog() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + XCTAssertTrue( + addressBarBookmarkButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Address bar bookmark button didn't load with the expected title in a reasonable timeframe." + ) + let addressBarBookmarkButtonValue = try? XCTUnwrap( + addressBarBookmarkButton.value as? String, + "It wasn't possible to get the value of the bookmarks management accessory image view as String" + ) + XCTAssertEqual( + addressBarBookmarkButtonValue, + "Bookmarked", + "The accessibility value of the Address Bar Bookmark Button must be \"Bookmarked\"." + ) + + addressBarBookmarkButton.click() + let bookMarkDialogBookmarkTitle = app.textFields[pageTitle] + + XCTAssertTrue( + bookMarkDialogBookmarkTitle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarked url title wasn't found in the bookmark dialog in a bookmarked state in a reasonable timeframe." + ) + } + + func test_bookmarksTab_canBeViewedViaMenuItemManageBookmarks() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + bookmarksMenu.clickAfterExistenceTestSucceeds() + + manageBookmarksMenuItem.clickAfterExistenceTestSucceeds() + + XCTAssertTrue( + bookmarksTabPopup.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmarks tab bookmarks popup didn't load with the expected title in a reasonable timeframe." + ) + } + + func test_favorites_appearWithTheCorrectIndicatorInBookmarksTab() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: false) + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + if bookmarksDialogAddToFavoritesCheckboxValue == false { + bookmarksDialogAddToFavoritesCheckbox.click() + } + app.typeKey(.escape, modifierFlags: []) // Exit dialog + + bookmarksMenu.clickAfterExistenceTestSucceeds() + manageBookmarksMenuItem.clickAfterExistenceTestSucceeds() + XCTAssertTrue( + bookmarksTabPopup.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmarks tab bookmarks popup didn't load with the expected title in a reasonable timeframe." + ) + XCTAssertTrue( + bookmarksManagementAccessoryImageView.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmarks accessory view favorites indicator didn't load with the expected title in a reasonable timeframe." + ) + let bookmarksManagementAccessoryImageViewValue = try? XCTUnwrap( + bookmarksManagementAccessoryImageView.value as? String, + "It wasn't possible to get the value of the bookmarks management accessory image view as String" + ) + + XCTAssertEqual( + bookmarksManagementAccessoryImageViewValue, + "Favorited", + "The accessibility value of the favorite accessory view on the bookmark management view must be \"Favorited\"." + ) + } + + func test_favorites_appearInNewTabFavoritesGrid() throws { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: false) + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + if bookmarksDialogAddToFavoritesCheckboxValue == false { + bookmarksDialogAddToFavoritesCheckbox.click() + } + app.typeKey(.escape, modifierFlags: []) // Exit dialog + + toggleBookmarksBarShowFavoritesOn() + let unwrappedPageTitle = try XCTUnwrap(pageTitle, "It wasn't possible to unwrap pageTitle") + let firstFavoriteInGridMatchingTitle = app.staticTexts["HomePage.Models.FavoriteModel.\(unwrappedPageTitle)"].firstMatch + + XCTAssertTrue( + firstFavoriteInGridMatchingTitle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The favorited item in the grid did not become available in a reasonable timeframe." + ) + } + + func test_favorites_canBeRemovedFromAddressBarBookmarkDialog() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: false) + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + if bookmarksDialogAddToFavoritesCheckboxValue == false { + bookmarksDialogAddToFavoritesCheckbox.click() // Favorite the bookmark + } + app.typeKey(.escape, modifierFlags: []) // Exit dialog + + XCTAssertTrue( + addressBarBookmarkButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Address bar bookmark button didn't load with the expected title in a reasonable timeframe." + ) + let addressBarBookmarkButtonValue = try? XCTUnwrap( + addressBarBookmarkButton.value as? String, + "It wasn't possible to get the value of the bookmarks management accessory image view as String" + ) + XCTAssertEqual( + addressBarBookmarkButtonValue, + "Bookmarked", + "The accessibility value of the Address Bar Bookmark Button must be \"Bookmarked\"." + ) + addressBarBookmarkButton.click() + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarksDialogAddToFavoritesCheckboxNewValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + if bookmarksDialogAddToFavoritesCheckboxNewValue == true { + bookmarksDialogAddToFavoritesCheckbox.click() // Unfavorite the bookmark + } + let bookmarksDialogAddToFavoritesCheckboxLastValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + let addToFavoritesLabel = "Add to Favorites" + + XCTAssertEqual( + bookmarksDialogAddToFavoritesCheckboxLastValue, + false, + "The favorite checkbox in the add bookmark dialog must now be unchecked" + ) + XCTAssertEqual( + bookmarksDialogAddToFavoritesCheckbox.label, + addToFavoritesLabel, + "The label of the add to favorites checkbox must now be \"\(addToFavoritesLabel)\"" + ) + } + + func test_favorites_canBeRemovedFromManageBookmarks() { + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: false) + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + if bookmarksDialogAddToFavoritesCheckboxValue == false { + bookmarksDialogAddToFavoritesCheckbox.click() // Favorite the bookmark + } + app.typeKey(.escape, modifierFlags: []) // Exit dialog + + bookmarksMenu.clickAfterExistenceTestSucceeds() + manageBookmarksMenuItem.clickAfterExistenceTestSucceeds() + bookmarkTableCellViewFavIconImageView.hoverAfterExistenceTestSucceeds() + bookmarkTableCellViewMenuButton.clickAfterExistenceTestSucceeds() + contextualMenuRemoveBookmarkFromFavoritesMenuItem.clickAfterExistenceTestSucceeds() + + XCTAssertTrue( + bookmarksManagementAccessoryImageView.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "Bookmarks accessory view favorites indicator didn't disappear from the view in a reasonable timeframe." + ) + } + + func test_favorites_canBeRemovedFromNewTabViaContextClick() throws { + toggleBookmarksBarShowFavoritesOn() + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: false) + XCTAssertTrue( + bookmarksDialogAddToFavoritesCheckbox.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The add to favorites checkbox in the add bookmark dialog didn't appear with the expected title in a reasonable timeframe." + ) + let bookmarksDialogAddToFavoritesCheckboxValue = try? XCTUnwrap( + bookmarksDialogAddToFavoritesCheckbox.value as? Bool, + "It wasn't possible to get the value of the bookmarks dialog's add to favorites checkbox as Bool" + ) + if bookmarksDialogAddToFavoritesCheckboxValue == false { + bookmarksDialogAddToFavoritesCheckbox.click() // Favorite the bookmark + } + app.typeKey(.escape, modifierFlags: []) // Exit dialog + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close all windows + app.typeKey("n", modifierFlags: .command) // New window + + let unwrappedPageTitle = try XCTUnwrap(pageTitle, "It wasn't possible to unwrap pageTitle") + let firstFavoriteInGridMatchingTitle = app.staticTexts["HomePage.Models.FavoriteModel.\(unwrappedPageTitle)"].firstMatch + XCTAssertTrue( + firstFavoriteInGridMatchingTitle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The favorited item in the grid did not become available in a reasonable timeframe." + ) + firstFavoriteInGridMatchingTitle.rightClick() + removeFavoritesContextMenuItem.clickAfterExistenceTestSucceeds() + + XCTAssertTrue( + firstFavoriteInGridMatchingTitle.waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "The favorited item in the grid did not disappear in a reasonable timeframe." + ) + } + + func test_bookmark_canBeRemovedViaAddressBarIconClick() { + toggleShowBookmarksBarAlwaysOn() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + + addressBarBookmarkButton.clickAfterExistenceTestSucceeds() + defaultBookmarkOtherButton.clickAfterExistenceTestSucceeds() + app.typeKey(.escape, modifierFlags: []) // Exit dialog + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + + XCTAssertTrue( + app.staticTexts[pageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "Since there is no bookmark of the page, and we show bookmarks in the bookmark bar, the title of the page should not appear in a new browser window anywhere." + ) + } + + func test_bookmark_canBeRemovedFromBookmarksTabViaHoverAndContextMenu() { + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + + bookmarksMenu.clickAfterExistenceTestSucceeds() + manageBookmarksMenuItem.clickAfterExistenceTestSucceeds() + bookmarkTableCellViewFavIconImageView.hoverAfterExistenceTestSucceeds() + bookmarkTableCellViewMenuButton.clickAfterExistenceTestSucceeds() + contextualMenuDeleteBookmarkMenuItem.clickAfterExistenceTestSucceeds() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + + XCTAssertTrue( + app.staticTexts[pageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "Since there is no bookmark of the page, and we show bookmarks in the bookmark bar, the title of the page should not appear in a new browser window anywhere." + ) + } + + func test_bookmark_canBeRemovedFromBookmarksBarViaRightClick() { +// This test uses coordinates (instead of accessibility IDs) to address the elements of the right click. As the writer of this test, I see this +// as a fragile test hook. However, I think it is preferable to making changes to the UI element it tests for this test alone. The reason is +// that the bookmark item on the bookmark bar isn't yet an accessibility-enabled UI element and doesn't appear to have a natural anchor point +// from which we can set its accessibility values without redesigning it. However, redesigning a road-tested UI element for a single test isn't a +// good idea, since the road-testing is also (valuable) testing and we don't want a single test to be the driver of a possible behavioral +// change in existing interface. +// +// My advice is to keep this as-is for now, with an awareness that it can fail if the coordinates of the items in the right-click menu change, +// or if the system where the testing is done has accessibility settings which change scaling. When the time comes to update this element, into +// SwiftUI, or into a general accessibility revision (for end-user accessibility rather than UI test accessibility), that will be the natural +// time to correct this test and give it accessibility ID access. Until then, I have added some hinting in the failure reason to explain why +// this test can fail while the app is working correctly. -Halle Winkler + + toggleShowBookmarksBarAlwaysOn() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + openSiteToBookmark(bookmarkingViaDialog: true, escapingDialog: true) + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + + XCTAssertTrue( + bookmarksBarCollectionView.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarks bar collection view failed to become available in a reasonable timeframe." + ) + let bookmarkBarBookmarkIcon = bookmarksBarCollectionView.images.firstMatch + XCTAssertTrue( + bookmarkBarBookmarkIcon.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The bookmarks bar bookmark icon failed to become available in a reasonable timeframe." + ) + let bookmarkBarBookmarkIconCoordinate = bookmarkBarBookmarkIcon.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) + let deleteContextMenuItemCoordinate = bookmarkBarBookmarkIcon.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 9.0)) + bookmarkBarBookmarkIconCoordinate.rightClick() + deleteContextMenuItemCoordinate.click() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) + app.typeKey("n", modifierFlags: .command) + + XCTAssertTrue( + app.staticTexts[pageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "Since there is no bookmark of the page, and we show bookmarks in the bookmark bar, the title of the page should not appear in a new browser window anywhere. In this specific test, it is highly probable that the reason for a failure (when this area of the app appears to be working correctly) is the contextual menu being rearranged, since it has to address the menu elements by coordinate." + ) + } +} + +private extension BookmarksAndFavoritesTests { + /// Reset the bookmarks so we can rely on a single bookmark's existence + func resetBookmarks() { + app.typeKey("n", modifierFlags: [.command]) // Can't use debug menu without a window + XCTAssertTrue( + resetBookMarksMenuItem.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Reset bookmarks menu item didn't become available in a reasonable timeframe." + ) + resetBookMarksMenuItem.click() + } + + /// Make sure that we can reply on the bookmarks bar always appearing + func toggleShowBookmarksBarAlwaysOn() { + app.typeKey(",", modifierFlags: [.command]) // Open settings + + XCTAssertTrue( + settingsAppearanceButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The user settings appearance section button didn't become available in a reasonable timeframe." + ) + settingsAppearanceButton.click(forDuration: 0.5, thenDragTo: settingsAppearanceButton) + XCTAssertTrue( + showBookmarksBarPreferenceToggle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The toggle for showing the bookmarks bar didn't become available in a reasonable timeframe." + ) + + let showBookmarksBarIsChecked = try? XCTUnwrap( + showBookmarksBarPreferenceToggle.value as? Bool, + "It wasn't possible to get the \"Show bookmarks bar\" value as a Bool" + ) + if showBookmarksBarIsChecked == false { + showBookmarksBarPreferenceToggle.click() + } + XCTAssertTrue( + showBookmarksBarPopup.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Show Bookmarks Bar\" popup button didn't become available in a reasonable timeframe." + ) + showBookmarksBarPopup.click() + XCTAssertTrue( + showBookmarksBarAlways.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The \"Show Bookmarks Bar Always\" button didn't become available in a reasonable timeframe." + ) + showBookmarksBarAlways.click() + } + + /// Make sure that appearance tab has been used to set "show favorites" to true + func toggleBookmarksBarShowFavoritesOn() { + app.typeKey(",", modifierFlags: [.command]) // Open settings + + XCTAssertTrue( + settingsAppearanceButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The user settings appearance section button didn't become available in a reasonable timeframe." + ) + settingsAppearanceButton.click(forDuration: 0.5, thenDragTo: settingsAppearanceButton) + + XCTAssertTrue( + showFavoritesPreferenceToggle.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The user settings appearance section show favorites toggle didn't become available in a reasonable timeframe." + ) + let showFavoritesPreferenceToggleIsChecked = showFavoritesPreferenceToggle.value as? Bool + if showFavoritesPreferenceToggleIsChecked == false { // If untoggled, + showFavoritesPreferenceToggle.click() // Toggle "show favorites" + } + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close settings and everything else + app.typeKey("n", modifierFlags: .command) // New window + } + + /// Open the initial site to be bookmarked, bookmarking it and/or escaping out of the dialog only if needed + /// - Parameter bookmarkingViaDialog: open bookmark dialog, adding bookmark + /// - Parameter escapingDialog: `esc` key to leave dialog + func openSiteToBookmark(bookmarkingViaDialog: Bool, escapingDialog: Bool) { + XCTAssertTrue( + addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The address bar text field didn't become available in a reasonable timeframe." + ) + addressBarTextField.typeURL(urlForBookmarksBar) + XCTAssertTrue( + app.windows.webViews[pageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Visited site didn't load with the expected title in a reasonable timeframe." + ) + if bookmarkingViaDialog { + app.typeKey("d", modifierFlags: [.command]) // Add bookmark + if escapingDialog { + app.typeKey(.escape, modifierFlags: []) // Exit dialog + } + } + } +} diff --git a/UITests/BookmarksBarTests.swift b/UITests/BookmarksBarTests.swift index 55eecbe52b..eacea0c9aa 100644 --- a/UITests/BookmarksBarTests.swift +++ b/UITests/BookmarksBarTests.swift @@ -55,7 +55,6 @@ class BookmarksBarTests: XCTestCase { resetBookmarksAndAddOneBookmark() app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows openSettingsAndSetShowBookmarksBarToUnchecked() - settingsWindow = app.windows.containing(.checkBox, identifier: "Preferences.AppearanceView.showBookmarksBarPreferenceToggle").firstMatch openSecondWindowAndVisitSite() siteWindow = app.windows.containing(.webView, identifier: pageTitle).firstMatch } diff --git a/UITests/Common/UITests.swift b/UITests/Common/UITests.swift index 40f28936f9..438cf5be4d 100644 --- a/UITests/Common/UITests.swift +++ b/UITests/Common/UITests.swift @@ -24,7 +24,7 @@ enum UITests { /// Timeout constants for different test requirements enum Timeouts { /// Mostly, we use timeouts to wait for element existence. This is about 3x longer than needed, for CI resilience - static let elementExistence: Double = 2.5 + static let elementExistence: Double = 5.0 /// The fire animation time has environmental dependencies, so we want to wait for completion so we don't try to type into it static let fireAnimation: Double = 30.0 } diff --git a/UITests/Common/XCUIElementExtension.swift b/UITests/Common/XCUIElementExtension.swift index 7e72c41a02..1e0d0ba991 100644 --- a/UITests/Common/XCUIElementExtension.swift +++ b/UITests/Common/XCUIElementExtension.swift @@ -62,4 +62,20 @@ extension XCUIElement { self.typeText("\r") } } + + func clickAfterExistenceTestSucceeds() { + XCTAssertTrue( + self.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "\(self.debugDescription) didn't load with the expected title in a reasonable timeframe." + ) + self.click() + } + + func hoverAfterExistenceTestSucceeds() { + XCTAssertTrue( + self.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "\(self.debugDescription) didn't load with the expected title in a reasonable timeframe." + ) + self.hover() + } } From 0d7c9de8780696937690d0841d0a7fd73a88322c Mon Sep 17 00:00:00 2001 From: Tom Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:31:56 +0200 Subject: [PATCH 043/221] New daily pixel for history save failures (#2533) Task/Issue URL: https://app.asana.com/0/0/1206920829163329/f **Description**: Adding new daily pixel for history save failures to better understand how many devices are affected by a potential issue --- DuckDuckGo/History/Services/EncryptedHistoryStore.swift | 3 +++ DuckDuckGo/Statistics/PixelEvent.swift | 3 +++ DuckDuckGo/Statistics/PixelParameters.swift | 1 + 3 files changed, 7 insertions(+) diff --git a/DuckDuckGo/History/Services/EncryptedHistoryStore.swift b/DuckDuckGo/History/Services/EncryptedHistoryStore.swift index 361ab26a81..36838c0539 100644 --- a/DuckDuckGo/History/Services/EncryptedHistoryStore.swift +++ b/DuckDuckGo/History/Services/EncryptedHistoryStore.swift @@ -175,6 +175,7 @@ final class EncryptedHistoryStore: HistoryStoring { fetchedObjects = try self.context.fetch(fetchRequest) } catch { Pixel.fire(.debug(event: .historySaveFailed, error: error)) + Pixel.fire(.debug(event: .historySaveFailedDaily, error: error), limitTo: .dailyFirst) promise(.failure(error)) return } @@ -203,6 +204,7 @@ final class EncryptedHistoryStore: HistoryStoring { switch insertionResult { case .failure(let error): Pixel.fire(.debug(event: .historySaveFailed, error: error)) + Pixel.fire(.debug(event: .historySaveFailedDaily, error: error), limitTo: .dailyFirst) context.reset() promise(.failure(error)) case .success(let visitMOs): @@ -210,6 +212,7 @@ final class EncryptedHistoryStore: HistoryStoring { try self.context.save() } catch { Pixel.fire(.debug(event: .historySaveFailed, error: error)) + Pixel.fire(.debug(event: .historySaveFailedDaily, error: error), limitTo: .dailyFirst) context.reset() promise(.failure(HistoryStoreError.savingFailed)) return diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 9f789182da..902e5b0b5a 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -326,6 +326,7 @@ extension Pixel { case historyCleanEntriesFailed case historyCleanVisitsFailed case historySaveFailed + case historySaveFailedDaily case historyInsertVisitFailed case historyRemoveVisitsFailed @@ -816,6 +817,8 @@ extension Pixel.Event.Debug { return "history_clean_visits_failed" case .historySaveFailed: return "history_save_failed" + case .historySaveFailedDaily: + return "history_save_failed_daily" case .historyInsertVisitFailed: return "history_insert_visit_failed" case .historyRemoveVisitsFailed: diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 3b7d89b5c4..2fad773b42 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -256,6 +256,7 @@ extension Pixel.Event.Debug { .historyCleanEntriesFailed, .historyCleanVisitsFailed, .historySaveFailed, + .historySaveFailedDaily, .historyInsertVisitFailed, .historyRemoveVisitsFailed, .emailAutofillKeychainError, From ebb09c340f47ae13a3bd849caec8b769050d7e56 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:40:07 -0400 Subject: [PATCH 044/221] Change DAU pixel for VPN to weekly (#2552) Task/Issue URL: https://app.asana.com/0/0/1206685546588311/f **Description**: Reported `cohort` now uses the week number. --- .../MacPacketTunnelProvider.swift | 2 +- .../PixelKit/PixelKit+Parameters.swift | 2 + .../PixelKit/Sources/PixelKit/PixelKit.swift | 32 ++++--- .../Tests/PixelKitTests/PixelKitTests.swift | 96 ++++++++++++++++--- 4 files changed, 104 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index c9a8819faa..0689316a76 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -151,7 +151,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionActiveUser, frequency: .dailyOnly, - withAdditionalParameters: ["cohort": PixelKit.dateString(for: defaults.vpnFirstEnabled)], + withAdditionalParameters: [PixelKit.Parameters.vpnCohort: PixelKit.cohort(from: defaults.vpnFirstEnabled)], includeAppVersionParameter: true) case .reportConnectionAttempt(attempt: let attempt): switch attempt { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift index 777329156a..b692271709 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit+Parameters.swift @@ -66,6 +66,8 @@ public extension PixelKit { public static let vpnBreakageMetadata = "breakageMetadata" public static let reason = "reason" + + public static let vpnCohort = "cohort" } enum Values { diff --git a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift index eec1e1332c..f55ec17ba4 100644 --- a/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift +++ b/LocalPackages/PixelKit/Sources/PixelKit/PixelKit.swift @@ -72,13 +72,7 @@ public final class PixelKit { return calendar }() - private var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.calendar = defaultDailyPixelCalendar - dateFormatter.timeZone = defaultDailyPixelCalendar.timeZone - dateFormatter.dateFormat = "yyyy-MM-dd" - return dateFormatter - }() + private static let weeksToCoalesceCohort = 6 private let dateGenerator: () -> Date @@ -99,6 +93,8 @@ public final class PixelKit { source: String? = nil, defaultHeaders: [String: String], log: OSLog, + dailyPixelCalendar: Calendar? = nil, + dateGenerator: @escaping () -> Date = Date.init, defaults: UserDefaults, fireRequest: @escaping FireRequest) { shared = PixelKit(dryRun: dryRun, @@ -106,6 +102,8 @@ public final class PixelKit { source: source, defaultHeaders: defaultHeaders, log: log, + dailyPixelCalendar: dailyPixelCalendar, + dateGenerator: dateGenerator, defaults: defaults, fireRequest: fireRequest) } @@ -318,13 +316,23 @@ public final class PixelKit { onComplete: onComplete) } - private func dateString(for date: Date?) -> String? { - guard let date else { return nil } - return dateFormatter.string(from: date) + private func cohort(from cohortLocalDate: Date?, dateGenerator: () -> Date = Date.init) -> String? { + guard let cohortLocalDate, + let baseDate = pixelCalendar.date(from: .init(year: 2023, month: 1, day: 1)), + let weeksSinceCohortAssigned = pixelCalendar.dateComponents([.weekOfYear], from: cohortLocalDate, to: dateGenerator()).weekOfYear, + let assignedCohort = pixelCalendar.dateComponents([.weekOfYear], from: baseDate, to: cohortLocalDate).weekOfYear else { + return nil + } + + if weeksSinceCohortAssigned > Self.weeksToCoalesceCohort { + return "" + } else { + return "week-" + String(assignedCohort + 1) + } } - public static func dateString(for date: Date?) -> String { - Self.shared?.dateString(for: date) ?? "" + public static func cohort(from cohortLocalDate: Date?, dateGenerator: () -> Date = Date.init) -> String { + Self.shared?.cohort(from: cohortLocalDate, dateGenerator: dateGenerator) ?? "" } public static func pixelLastFireDate(event: Event) -> Date? { diff --git a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift index b9576d6a6a..3ce7447aae 100644 --- a/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift +++ b/LocalPackages/PixelKit/Tests/PixelKitTests/PixelKitTests.swift @@ -258,17 +258,18 @@ final class PixelKitTests: XCTestCase { // Run test pixelKit.fire(event, frequency: .dailyOnly) // Fired - timeMachine.travel(by: 60 * 60 * 2) - pixelKit.fire(event, frequency: .dailyOnly) // Skipped (2 hours since last fire) + timeMachine.travel(by: .hour, value: 2) + pixelKit.fire(event, frequency: .dailyOnly) // Skipped - timeMachine.travel(by: 60 * 60 * 24 + 1000) - pixelKit.fire(event, frequency: .dailyOnly) // Fired (24 hours + 1000 seconds since last fire) + timeMachine.travel(by: .day, value: 1) + timeMachine.travel(by: .hour, value: 2) + pixelKit.fire(event, frequency: .dailyOnly) // Fired - timeMachine.travel(by: 60 * 60 * 10) - pixelKit.fire(event, frequency: .dailyOnly) // Skipped (10 hours since last fire) + timeMachine.travel(by: .hour, value: 10) + pixelKit.fire(event, frequency: .dailyOnly) // Skipped - timeMachine.travel(by: 60 * 60 * 14) - pixelKit.fire(event, frequency: .dailyOnly) // Fired (24 hours since last fire) + timeMachine.travel(by: .day, value: 1) + pixelKit.fire(event, frequency: .dailyOnly) // Fired // Wait for expectations to be fulfilled wait(for: [fireCallbackCalled], timeout: 0.5) @@ -303,28 +304,93 @@ final class PixelKitTests: XCTestCase { // Run test pixelKit.fire(event, frequency: .justOnce) // Fired - timeMachine.travel(by: 60 * 60 * 2) + timeMachine.travel(by: .hour, value: 2) pixelKit.fire(event, frequency: .justOnce) // Skipped (already fired) - timeMachine.travel(by: 60 * 60 * 24 + 1000) + timeMachine.travel(by: .day, value: 1) + timeMachine.travel(by: .hour, value: 2) pixelKit.fire(event, frequency: .justOnce) // Skipped (already fired) - timeMachine.travel(by: 60 * 60 * 10) + timeMachine.travel(by: .hour, value: 10) pixelKit.fire(event, frequency: .justOnce) // Skipped (already fired) - timeMachine.travel(by: 60 * 60 * 14) + timeMachine.travel(by: .day, value: 1) pixelKit.fire(event, frequency: .justOnce) // Skipped (already fired) // Wait for expectations to be fulfilled wait(for: [fireCallbackCalled], timeout: 0.5) } + + func testVPNCohort() { + XCTAssertEqual(PixelKit.cohort(from: nil), "") + assertCohortEqual(.init(year: 2023, month: 1, day: 1), reportAs: "week-1") + assertCohortEqual(.init(year: 2024, month: 2, day: 24), reportAs: "week-60") + } + + private func assertCohortEqual(_ cohort: DateComponents, reportAs reportedCohort: String) { + var calendar = Calendar.current + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + calendar.locale = Locale(identifier: "en_US_POSIX") + + let cohort = calendar.date(from: cohort) + let timeMachine = TimeMachine(calendar: calendar, date: cohort) + + PixelKit.setUp(appVersion: "test", + defaultHeaders: [:], + log: .disabled, + dailyPixelCalendar: calendar, + dateGenerator: timeMachine.now, + defaults: userDefaults()) { _, _, _, _, _, _ in } + + // 1st week + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 2nd week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 3rd week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 4th week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 5th week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 6th week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 7th week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), reportedCohort) + + // 8th week + timeMachine.travel(by: .weekOfYear, value: 1) + XCTAssertEqual(PixelKit.cohort(from: cohort, dateGenerator: timeMachine.now), "") + } } private class TimeMachine { - private var date = Date(timeIntervalSince1970: 0) + private var date: Date + private let calendar: Calendar + + init(calendar: Calendar? = nil, date: Date? = nil) { + self.calendar = calendar ?? { + var calendar = Calendar.current + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + calendar.locale = Locale(identifier: "en_US_POSIX") + return calendar + }() + self.date = date ?? .init(timeIntervalSince1970: 0) + } - func travel(by timeInterval: TimeInterval) { - date = date.addingTimeInterval(timeInterval) + func travel(by component: Calendar.Component, value: Int) { + date = calendar.date(byAdding: component, value: value, to: now())! } func now() -> Date { From 4b9fa1a3e5c80234e6ac4a4be0e43d7415a05d70 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Tue, 9 Apr 2024 18:08:37 +0200 Subject: [PATCH 045/221] Update BSK with autofill 11.0.1 (#2588) Task/Issue URL: https://app.asana.com/0/1207038392239720/1207038392239720 Autofill Release: https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/11.0.1 BSK PR: duckduckgo/BrowserServicesKit#769 Description Updates Autofill to version 11.0.1. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index dfa32b6122..17154ba5d0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14452,7 +14452,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 133.0.0; + version = 133.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0e02888fb7..12fabf86b4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "c0b0cb55e7ac2f69d10452e1a5c06713155d798e", - "version" : "133.0.0" + "revision" : "39d74829150a9ecffea2f503c01851e54eda8ad1", + "version" : "133.0.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "6493e296934bf09277c03df45f11f4619711cb24", - "version" : "10.2.0" + "revision" : "6053999d6af384a716ab0ce7205dbab5d70ed1b3", + "version" : "11.0.1" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 46c094664b..9418e01778 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", exact: "133.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index d0ea0235c3..4e62507c16 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 556d4f308e..1c064eb962 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: "133.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 532ec92fc6bd42a85007b5868640601713e449cd Mon Sep 17 00:00:00 2001 From: Halle <378795+Halle@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:13:06 -0700 Subject: [PATCH 046/221] Adds a series of UI tests for State Restoration Task/Issue URL: https://app.asana.com/0/1199230911884351/1205717021705374/f Tech Design URL: CC: **Description**: Adds a series of UI tests for State Restoration **Steps to test this PR**: 1. Open the scheme **UI Tests** 2. Navigate to the test pane 3. Run `StateRestorationTests` **Note**: If this builds a `review` build that is treated as a first run on your system during every test (for instance, asking you where to put the application), please **build and run** the scheme once instead of running its tests, and go through the new app install first run steps once before running the tests, and answer any first install questions. **UI Tests general guidelines for everyone**: * It (unfortunately!) isn't possible to multitask while running UI tests on your local machine, because your mousing/interface usage will change or intercept screen interactions that the tests depend on. * Much as we all want UI testing to work on multi-monitor setups, we have noticed specific focus issues related to waiting for focus across two monitors simultaneously, so we have decided to test the tests on a single monitor. * We are currently testing with English as the app destination language. * If you experience test failures in your local environment, please always send the `xcresult` bundle so it is possible to view what happened differently on your machine, even if it seems like the same failure as a previous failure (it may be subtly different). Please give its screenshots or screen recording a look first before sending, just to rule out one-off failure causes like an unrelated app unexpectedly throwing up an update helper (or something similar) in front of where `XCUITest` is waiting for a UI element to exist. * Since the `xcresult` bundle will include a screen recording or screenshots, for your privacy, please consider things like your chats, calls, and open documents that you don't want to send to a contractor when you are running the tests. Thank you very much for your help! --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../View/PreferencesGeneralView.swift | 8 +- UITests/StateRestorationTests.swift | 134 ++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 UITests/StateRestorationTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 17154ba5d0..d7da5c939e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3303,6 +3303,7 @@ EE7295ED2A545C0A008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EC2A545C0A008C0991 /* NetworkProtection */; }; EE7295EF2A545C12008C0991 /* NetworkProtection in Frameworks */ = {isa = PBXBuildFile; productRef = EE7295EE2A545C12008C0991 /* NetworkProtection */; }; EE7F74912BB5D76600CD9456 /* BookmarksBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */; }; + EE9D81C32BC57A3700338BE3 /* StateRestorationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */; }; EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */; }; EEA3EEB32B24EC0600E8333A /* VPNLocationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */; }; EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; @@ -4777,6 +4778,7 @@ EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarTests.swift; sourceTree = ""; }; + EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateRestorationTests.swift; sourceTree = ""; }; EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLauncher.swift; sourceTree = ""; }; @@ -6562,6 +6564,7 @@ EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */, EE02D41B2BB460A600DBE6B3 /* BrowsingHistoryTests.swift */, EE0429DF2BA31D2F009EB20F /* FindInPageTests.swift */, + EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */, 7B4CE8E626F02134009134B1 /* TabBarTests.swift */, ); path = UITests; @@ -12282,6 +12285,7 @@ EE02D41A2BB4609900DBE6B3 /* UITests.swift in Sources */, EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, EE02D4212BB460FE00DBE6B3 /* StringExtension.swift in Sources */, + EE9D81C32BC57A3700338BE3 /* StateRestorationTests.swift in Sources */, EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */, EE02D4222BB4611A00DBE6B3 /* TestsURLExtension.swift in Sources */, 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */, diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 591961ee75..521893ba2e 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -39,11 +39,13 @@ extension Preferences { PreferencePaneSubSection { Picker(selection: $startupModel.restorePreviousSession, content: { Text(UserText.showHomePage).tag(false) - .padding(.bottom, 4) + .padding(.bottom, 4).accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker.openANewWindow") Text(UserText.reopenAllWindowsFromLastSession).tag(true) + .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker.reopenAllWindowsFromLastSession") }, label: {}) - .pickerStyle(.radioGroup) - .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + .pickerStyle(.radioGroup) + .offset(x: PreferencesViews.Const.pickerHorizontalOffset) + .accessibilityIdentifier("PreferencesGeneralView.stateRestorePicker") } } diff --git a/UITests/StateRestorationTests.swift b/UITests/StateRestorationTests.swift new file mode 100644 index 0000000000..32f86a0d2e --- /dev/null +++ b/UITests/StateRestorationTests.swift @@ -0,0 +1,134 @@ +// +// StateRestorationTests.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 XCTest + +class StateRestorationTests: XCTestCase { + private var app: XCUIApplication! + private var firstPageTitle: String! + private var secondPageTitle: String! + private var firstURLForBookmarksBar: URL! + private var secondURLForBookmarksBar: URL! + private let titleStringLength = 12 + private var addressBarTextField: XCUIElement! + private var settingsGeneralButton: XCUIElement! + private var openANewWindowPreference: XCUIElement! + private var reopenAllWindowsFromLastSessionPreference: XCUIElement! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" + firstPageTitle = UITests.randomPageTitle(length: titleStringLength) + secondPageTitle = UITests.randomPageTitle(length: titleStringLength) + firstURLForBookmarksBar = UITests.simpleServedPage(titled: firstPageTitle) + secondURLForBookmarksBar = UITests.simpleServedPage(titled: secondPageTitle) + addressBarTextField = app.windows.textFields["AddressBarViewController.addressBarTextField"] + settingsGeneralButton = app.buttons["PreferencesSidebar.generalButton"] + openANewWindowPreference = app.radioButtons["PreferencesGeneralView.stateRestorePicker.openANewWindow"] + reopenAllWindowsFromLastSessionPreference = app.radioButtons["PreferencesGeneralView.stateRestorePicker.reopenAllWindowsFromLastSession"] + + app.launch() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Let's enforce a single window + app.typeKey("n", modifierFlags: .command) + } + + override func tearDownWithError() throws { + app.terminate() + } + + func test_tabStateAtRelaunch_shouldContainTwoSitesVisitedInPreviousSession_whenReopenAllWindowsFromLastSessionIsSet() { + app.typeKey(",", modifierFlags: [.command]) // Open settings + settingsGeneralButton.click(forDuration: 0.5, thenDragTo: settingsGeneralButton) + reopenAllWindowsFromLastSessionPreference.clickAfterExistenceTestSucceeds() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows + app.typeKey("n", modifierFlags: .command) + XCTAssertTrue( + addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The address bar text field didn't become available in a reasonable timeframe." + ) + addressBarTextField.typeURL(firstURLForBookmarksBar) + XCTAssertTrue( + app.windows.webViews[firstPageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Site didn't load with the expected title in a reasonable timeframe." + ) + app.typeKey("t", modifierFlags: [.command]) + app.typeURL(secondURLForBookmarksBar) + XCTAssertTrue( + app.windows.webViews[secondPageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Site didn't load with the expected title in a reasonable timeframe." + ) + + app.terminate() + app.launch() + + XCTAssertTrue( + app.windows.webViews[secondPageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Second visited site wasn't found in a webview with the expected title in a reasonable timeframe." + ) + app.typeKey("w", modifierFlags: [.command]) + XCTAssertTrue( + app.windows.webViews[firstPageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "First visited site wasn't found in a webview with the expected title in a reasonable timeframe." + ) + } + + func test_tabStateAtRelaunch_shouldContainNoSitesVisitedInPreviousSession_whenReopenAllWindowsFromLastSessionIsUnset() { + app.typeKey(",", modifierFlags: [.command]) // Open settings + settingsGeneralButton.click(forDuration: 0.5, thenDragTo: settingsGeneralButton) + openANewWindowPreference.clickAfterExistenceTestSucceeds() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Close windows + app.typeKey("n", modifierFlags: .command) + XCTAssertTrue( + addressBarTextField.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The address bar text field didn't become available in a reasonable timeframe." + ) + addressBarTextField.typeURL(firstURLForBookmarksBar) + XCTAssertTrue( + app.windows.webViews[firstPageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Site didn't load with the expected title in a reasonable timeframe." + ) + app.typeKey("t", modifierFlags: [.command]) + app.typeURL(secondURLForBookmarksBar) + XCTAssertTrue( + app.windows.webViews[secondPageTitle].waitForExistence(timeout: UITests.Timeouts.elementExistence), + "Site didn't load with the expected title in a reasonable timeframe." + ) + + app.terminate() + app.launch() + + XCTAssertTrue( + app.windows.webViews[secondPageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "Second visited site from previous session should not be in any webview." + ) + XCTAssertTrue( + app.windows.webViews[firstPageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "First visited site from previous session should not be in any webview." + ) + app.typeKey("w", modifierFlags: [.command]) + XCTAssertTrue( + app.windows.webViews[firstPageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "First visited site from previous session should not be in any webview." + ) + XCTAssertTrue( + app.windows.webViews[secondPageTitle].waitForNonExistence(timeout: UITests.Timeouts.elementExistence), + "Second visited site from previous session should not be in any webview." + ) + } +} From 04e56c9ecf961163b269c9db65399a2012685095 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 9 Apr 2024 18:01:43 +0000 Subject: [PATCH 047/221] Bump version to 1.82.1 (155) --- Configuration/BuildNumber.xcconfig | 2 +- Configuration/Version.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 92feb2ccd4..8d555e6249 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 152 +CURRENT_PROJECT_VERSION = 155 diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 4aebe00d0f..0868ba0c18 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.82.0 +MARKETING_VERSION = 1.82.1 From fb1e132c664f973e1c0acd88e6c609c3c6b00b14 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 9 Apr 2024 20:43:37 +0200 Subject: [PATCH 048/221] Hotfixing an issue with VPN visibility for certain waitlist users (#2591) Task/Issue URL: https://app.asana.com/0/0/1207040067636855/f Description Fixes an issue where some users were seeing waitlist UI when the subscription was avilable. --- .../View/NavigationBarViewController.swift | 30 ++++++++++++------- .../NetworkProtectionFeatureDisabler.swift | 4 ++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 2148a66473..fbf3cc1ae8 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -338,18 +338,26 @@ final class NavigationBarViewController: NSViewController { } #endif - // 1. If the user is on the waitlist but hasn't been invited or accepted terms and conditions, show the waitlist screen. - // 2. If the user has no waitlist state but has an auth token, show the NetP popover. - // 3. If the user has no state of any kind, show the waitlist screen. - - if NetworkProtectionWaitlist().shouldShowWaitlistViewController { - NetworkProtectionWaitlistViewControllerPresenter.show() - DailyPixel.fire(pixel: .networkProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) - } else if NetworkProtectionKeychainTokenStore().isFeatureActivated { - popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) + // Note: the following code is quite contrived but we're aiming to hotfix issues without mixing subscription and + // waitlist logic. This should be cleaned up once waitlist can safely be removed. + + if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { + if NetworkProtectionKeychainTokenStore().isFeatureActivated { + popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) + } } else { - NetworkProtectionWaitlistViewControllerPresenter.show() - DailyPixel.fire(pixel: .networkProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) + // 1. If the user is on the waitlist but hasn't been invited or accepted terms and conditions, show the waitlist screen. + // 2. If the user has no waitlist state but has an auth token, show the NetP popover. + // 3. If the user has no state of any kind, show the waitlist screen. + + if NetworkProtectionWaitlist().shouldShowWaitlistViewController { + NetworkProtectionWaitlistViewControllerPresenter.show() + DailyPixel.fire(pixel: .networkProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) + } else if NetworkProtectionKeychainTokenStore().isFeatureActivated { + popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) + } else { + NetworkProtectionWaitlistViewControllerPresenter.show() + } } } #endif diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index ba12c9cdd2..d7cc337991 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -76,6 +76,9 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling @MainActor @discardableResult func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool { + // We can do this optimistically as it has little if any impact. + unpinNetworkProtection() + // To disable NetP we need the login item to be running // This should be fine though as we'll disable them further down below guard canUninstall(includingSystemExtension: uninstallSystemExtension) else { @@ -85,7 +88,6 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling isDisabling = true defer { - unpinNetworkProtection() resetUserDefaults(uninstallSystemExtension: uninstallSystemExtension) } From e562f1166d12ea4d095b45ae28f210a2fc7f215f Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 9 Apr 2024 22:32:31 +0200 Subject: [PATCH 049/221] Remove non-existent networkProtectionWaitlistIntroDisplayed pixel --- DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 55eb55aa20..0274af6b08 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -323,7 +323,6 @@ final class NavigationBarViewController: NSViewController { if NetworkProtectionWaitlist().shouldShowWaitlistViewController { NetworkProtectionWaitlistViewControllerPresenter.show() - DailyPixel.fire(pixel: .networkProtectionWaitlistIntroDisplayed, frequency: .dailyAndCount) } else if NetworkProtectionKeychainTokenStore().isFeatureActivated { popovers.toggleNetworkProtectionPopover(usingView: networkProtectionButton, withDelegate: networkProtectionButtonModel) } else { From faadae581ef25e7e2734b1a45da1abc7ccc36282 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Tue, 9 Apr 2024 20:42:26 +0000 Subject: [PATCH 050/221] Bump version to 1.83.0 (156) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 8d555e6249..1896f3713a 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 155 +CURRENT_PROJECT_VERSION = 156 From 22484b7755599b29e400c0cd9337179a6586fd35 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 9 Apr 2024 21:48:02 -0700 Subject: [PATCH 051/221] Hide the `DuckDuckGo on Other Platforms` section on the App Store (#2592) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207033225544215/f Description: This PR updates the "DuckDuckGo on Other Platforms" section of the Settings page to only show on Sparkle builds. This is done because Apple forbids references to other platforms on the App Store. --- DuckDuckGo/Preferences/Model/PreferencesSection.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 161ffb3440..edf564d458 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -49,7 +49,12 @@ struct PreferencesSection: Hashable, Identifiable { return panes }() +#if APPSTORE + // App Store guidelines don't allow references to other platforms, so the Mac App Store build omits the otherPlatforms section. + let otherPanes: [PreferencePaneIdentifier] = [.about] +#else let otherPanes: [PreferencePaneIdentifier] = [.about, .otherPlatforms] +#endif var sections: [PreferencesSection] = [ .init(id: .privacyProtections, panes: privacyPanes), From 86f799f8e0a2bf4babeb693490357649c4eb2257 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Wed, 10 Apr 2024 05:12:08 +0000 Subject: [PATCH 052/221] Bump version to 1.83.0 (157) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 1896f3713a..eb40c52354 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 156 +CURRENT_PROJECT_VERSION = 157 From 39922beda185a96d7b1a2b4fcb37ecdb80cf0d6f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 10 Apr 2024 10:36:38 +0500 Subject: [PATCH 053/221] Percent-decode download filenames (#2584) Task/Issue URL: https://app.asana.com/0/1199230911884351/1202199027702557/f --- DuckDuckGo/Common/Extensions/StringExtension.swift | 4 ++++ DuckDuckGo/FileDownload/Extensions/WKWebView+Download.swift | 6 +----- DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index eeb8e5bedb..6128d9906a 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -71,6 +71,10 @@ extension String { return result } + func replacingInvalidFileNameCharacters(with replacement: String = "_") -> String { + replacingOccurrences(of: "[~#@*+%{}<>\\[\\]|\"\\_^\\/:\\\\]", with: replacement, options: .regularExpression) + } + init(_ staticString: StaticString) { self = staticString.withUTF8Buffer { String(decoding: $0, as: UTF8.self) diff --git a/DuckDuckGo/FileDownload/Extensions/WKWebView+Download.swift b/DuckDuckGo/FileDownload/Extensions/WKWebView+Download.swift index ad211f4eba..757f50f845 100644 --- a/DuckDuckGo/FileDownload/Extensions/WKWebView+Download.swift +++ b/DuckDuckGo/FileDownload/Extensions/WKWebView+Download.swift @@ -24,11 +24,7 @@ import WebKit extension WKWebView { var suggestedFilename: String? { - guard let title = self.title?.replacingOccurrences(of: "[~#@*+%{}<>\\[\\]|\"\\_^\\/:\\\\]", - with: "_", - options: .regularExpression), - !title.isEmpty - else { + guard let title = self.title?.replacingInvalidFileNameCharacters(), !title.isEmpty else { return url?.suggestedFilename } return title.appending(".html") diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift index a39f173fa1..ca3d3e26c9 100644 --- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift +++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift @@ -560,7 +560,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { progress.totalUnitCount = response.expectedContentLength } - var suggestedFilename = suggestedFilename + var suggestedFilename = (suggestedFilename.removingPercentEncoding ?? suggestedFilename).replacingInvalidFileNameCharacters() // sometimes suggesteFilename has an extension appended to already present URL file extension // e.g. feed.xml.rss for www.domain.com/rss.xml if let urlSuggestedFilename = response.url?.suggestedFilename, From 77f8b144ff9b4357beab09463cc3af7a168854b6 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 10 Apr 2024 10:39:07 +0500 Subject: [PATCH 054/221] Update Privacy Dashboard URL on navigation commit (#2583) Task/Issue URL: https://app.asana.com/0/1177771139624306/1205436170208663/f --- .../TabExtensions/PrivacyDashboardTabExtension.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift index ab33546a06..070374f0a2 100644 --- a/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/PrivacyDashboardTabExtension.swift @@ -136,13 +136,14 @@ extension PrivacyDashboardTabExtension: NavigationResponder { } @MainActor - func didReceiveRedirect(_ navigationAction: NavigationAction, for navigation: Navigation) { - resetDashboardInfo(for: navigationAction.url, didGoBackForward: false) + func didCommit(_ navigation: Navigation) { + resetDashboardInfo(for: navigation.url, didGoBackForward: navigation.navigationAction.navigationType.isBackForward) } - @MainActor - func didStart(_ navigation: Navigation) { - resetDashboardInfo(for: navigation.url, didGoBackForward: navigation.navigationAction.navigationType.isBackForward) + func navigationDidFinish(_ navigation: Navigation) { + if privacyInfo?.url != navigation.url { + resetDashboardInfo(for: navigation.url, didGoBackForward: navigation.navigationAction.navigationType.isBackForward) + } } } From b73b0b3f54bb8143861ee88f0fb2fa125d7c9334 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 10 Apr 2024 12:02:29 +0500 Subject: [PATCH 055/221] Fix Open Downloads not working (#2576) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207020636198102/f --- .../Model/DownloadViewModel.swift | 2 +- .../View/DownloadsViewController.swift | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift b/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift index cdcdadf330..d22e8a9e6e 100644 --- a/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift +++ b/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift @@ -75,7 +75,7 @@ final class DownloadViewModel { } func update(with item: DownloadListItem) { - self.localURL = item.destinationURL + self.localURL = item.tempURL == nil ? item.destinationURL : nil // only return destination file URL for completed downloads self.filename = item.fileName let oldState = self.state let newState = State(item: item, shouldAnimateOnAppear: state.shouldAnimateOnAppear ?? true) diff --git a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift index fb33a0788d..dac2e0bec7 100644 --- a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift +++ b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift @@ -158,35 +158,43 @@ final class DownloadsViewController: NSViewController { @IBAction func openDownloadsFolderAction(_ sender: Any) { let prefs = DownloadsPreferences.shared + let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] var url: URL? var itemToSelect: URL? if prefs.alwaysRequestDownloadLocation { - url = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + url = prefs.lastUsedCustomDownloadLocation + // reveal the last completed download if let lastDownloaded = viewModel.items.first/* last added */(where: { // should still exist - $0.localURL != nil && FileManager.default.fileExists(atPath: $0.localURL!.deletingLastPathComponent().path) + if let url = $0.localURL, FileManager.default.fileExists(atPath: url.path) { true } else { false } }), let lastDownloadedURL = lastDownloaded.localURL, - // if no downloads are from the default Downloads folder - open the last downloaded item folder !viewModel.items.contains(where: { $0.localURL?.deletingLastPathComponent().path == url?.path }) || url == nil { url = lastDownloadedURL.deletingLastPathComponent() // select last downloaded item itemToSelect = lastDownloadedURL - } /* else fallback to default User‘s Downloads */ + } /* else fallback to the last location chosen in the Save Panel */ } else { // open preferred downlod location - url = prefs.effectiveDownloadLocation ?? FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first + url = prefs.effectiveDownloadLocation } - guard let url else { return } + let folder = url ?? downloads + + _=NSWorkspace.shared.selectFile(itemToSelect?.path, inFileViewerRootedAtPath: folder.path) + // hack for the sandboxed environment: + // when we have no permission to open a folder we don‘t have access to + // try to guess a file that would most probably exist and reveal it: it‘s the ".DS_Store" file + || NSWorkspace.shared.selectFile(folder.appendingPathComponent(".DS_Store").path, inFileViewerRootedAtPath: folder.path) + // fallback to default Downloads folder + || NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: downloads.path) self.dismiss() - NSWorkspace.shared.selectFile(itemToSelect?.path, inFileViewerRootedAtPath: url.path) } @IBAction func clearDownloadsAction(_ sender: Any) { From c3b0c7ba6e42f682f696a80bbcec8777c68d1152 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 10 Apr 2024 12:10:20 +0500 Subject: [PATCH 056/221] Allow choosing downloads location in App Store builds (#2532) Task/Issue URL: https://app.asana.com/0/1199230911884351/1205665411589753/f - https://app.asana.com/0/0/1205084529216312/f - https://app.asana.com/0/1199178362774117/1201154025406383/f - https://app.asana.com/0/1199230911884351/1207020636198094/f - https://app.asana.com/0/1199230911884351/1207020636198108/f --- Configuration/App/DuckDuckGo.xcconfig | 8 +- Configuration/AppStore.xcconfig | 8 +- DuckDuckGo.xcodeproj/project.pbxproj | 22 ++ .../Extensions/FileManagerExtension.swift | 12 + .../Common/Extensions/URLExtension.swift | 44 +++ DuckDuckGo/Common/Logging/Logging.swift | 42 +++ .../FileDownload/Model/FilePresenter.swift | 256 ++++++++++-------- .../Model/NSURL+sandboxExtensionRetainCount.m | 40 +++ .../SecurityScopedFileURLController.swift | 179 ++++++++++++ .../Model/WebKitDownloadTask.swift | 101 ++++--- .../Services/DownloadListCoordinator.swift | 32 ++- .../Model/DownloadsPreferences.swift | 64 +++-- .../View/PreferencesGeneralView.swift | 4 +- .../Tab/View/BrowserTabViewController.swift | 19 +- DuckDuckGo/Tab/View/WebView.swift | 1 + .../Downloads/DownloadsIntegrationTests.swift | 18 +- .../FileDownload/FilePresenterTests.swift | 81 ++---- .../DownloadsPreferencesTests.swift | 9 +- sandbox-test-tool/SandboxTestTool.swift | 39 ++- 19 files changed, 714 insertions(+), 265 deletions(-) create mode 100644 DuckDuckGo/FileDownload/Model/NSURL+sandboxExtensionRetainCount.m create mode 100644 DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift diff --git a/Configuration/App/DuckDuckGo.xcconfig b/Configuration/App/DuckDuckGo.xcconfig index e0490eb81d..778bc1e09d 100644 --- a/Configuration/App/DuckDuckGo.xcconfig +++ b/Configuration/App/DuckDuckGo.xcconfig @@ -34,10 +34,10 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = MacOS Browser PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = MacOS Browser Product Review -GCC_PREPROCESSOR_DEFINITIONS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 -GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 DEBUG=1 CI=1 $(inherited) -GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 DEBUG=1 $(inherited) -GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 REVIEW=1 $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) +GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 DEBUG=1 CI=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 DEBUG=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION=1 REVIEW=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) $(inherited) SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION $(FEATURE_FLAGS) SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION DEBUG CI $(FEATURE_FLAGS) diff --git a/Configuration/AppStore.xcconfig b/Configuration/AppStore.xcconfig index 1fbb567afd..30ec838615 100644 --- a/Configuration/AppStore.xcconfig +++ b/Configuration/AppStore.xcconfig @@ -23,10 +23,10 @@ MAIN_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(MAIN_BUNDLE_IDENTIFIER_PREFIX).d MAIN_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(MAIN_BUNDLE_IDENTIFIER_PREFIX).debug MAIN_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(MAIN_BUNDLE_IDENTIFIER_PREFIX).review -GCC_PREPROCESSOR_DEFINITIONS[arch=*][sdk=*] = APPSTORE=1 -GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = APPSTORE=1 DEBUG=1 CI=1 $(inherited) -GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = APPSTORE=1 DEBUG=1 $(inherited) -GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = APPSTORE=1 REVIEW=1 $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[arch=*][sdk=*] = APPSTORE=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) +GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = APPSTORE=1 DEBUG=1 CI=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = APPSTORE=1 DEBUG=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = APPSTORE=1 REVIEW=1 SWIFT_OBJC_INTERFACE_HEADER_NAME=$(SWIFT_OBJC_INTERFACE_HEADER_NAME) $(inherited) MACOSX_DEPLOYMENT_TARGET = 12.3 diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d7da5c939e..0ece6426d8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3063,6 +3063,12 @@ B6ABC5962B4861D4008343B9 /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABC5952B4861D4008343B9 /* FocusableTextField.swift */; }; B6ABC5972B4861D4008343B9 /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABC5952B4861D4008343B9 /* FocusableTextField.swift */; }; B6ABC5982B4861D4008343B9 /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABC5952B4861D4008343B9 /* FocusableTextField.swift */; }; + B6ABD0CA2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */; }; + B6ABD0CB2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */; }; + B6ABD0CC2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */; }; + B6ABD0CE2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */; }; + B6ABD0CF2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */; }; + B6ABD0D02BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */; }; B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AE39F029373AF200C37AA4 /* EmptyAttributionRulesProver.swift */; }; B6AE39F329374AEC00C37AA4 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B6AE39F229374AEC00C37AA4 /* OHHTTPStubs */; }; B6AE39F529374AEC00C37AA4 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B6AE39F429374AEC00C37AA4 /* OHHTTPStubsSwift */; }; @@ -3203,6 +3209,9 @@ B6EC37FC29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; }; B6EC37FD29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; }; B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = B6EC37FE29B8D915001ACE79 /* Configuration */; }; + B6EECB302BC3FA5A00B3CB77 /* SecurityScopedFileURLController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */; }; + B6EECB312BC3FAB100B3CB77 /* NSURL+sandboxExtensionRetainCount.m in Sources */ = {isa = PBXBuildFile; fileRef = B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */; }; + B6EECB322BC40A1400B3CB77 /* FileManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */; }; B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; }; B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; @@ -4645,6 +4654,8 @@ B6AAAC2C260330580029438D /* PublishedAfter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedAfter.swift; sourceTree = ""; }; B6AAAC3D26048F690029438D /* RandomAccessCollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomAccessCollectionExtension.swift; sourceTree = ""; }; B6ABC5952B4861D4008343B9 /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = ""; }; + B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityScopedFileURLController.swift; sourceTree = ""; }; + B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURL+sandboxExtensionRetainCount.m"; sourceTree = ""; }; B6AE39F029373AF200C37AA4 /* EmptyAttributionRulesProver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyAttributionRulesProver.swift; sourceTree = ""; }; B6AE74332609AFCE005B9B1A /* ProgressEstimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressEstimationTests.swift; sourceTree = ""; }; B6B040072B95C4C80085279D /* Downloads 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Downloads 2.xcdatamodel"; sourceTree = ""; }; @@ -6690,6 +6701,8 @@ B6C0B23826E742610031CB7F /* FileDownloadError.swift */, 856C98DE257014BD00A22F1F /* FileDownloadManager.swift */, B6E6B9E22BA1F5F1008AA7E1 /* FilePresenter.swift */, + B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */, + B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */, B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */, B6CC266B2BAD9CD800F53F8D /* FileProgressPresenter.swift */, ); @@ -10727,6 +10740,7 @@ 7B430EA22A71411A00BAC4A1 /* NetworkProtectionSimulateFailureMenu.swift in Sources */, 3706FBD5293F65D500E42796 /* TabCollection+NSSecureCoding.swift in Sources */, 3706FBD6293F65D500E42796 /* Instruments.swift in Sources */, + B6ABD0CF2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */, B62B483F2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, 569277C229DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */, 3706FBD7293F65D500E42796 /* ContentBlockerRulesLists.swift in Sources */, @@ -10872,6 +10886,7 @@ 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, 3706FC35293F65D500E42796 /* BookmarkNode.swift in Sources */, + B6ABD0CB2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, B6B140892ABDBCC1004F8E85 /* HoverTrackingArea.swift in Sources */, 3706FC36293F65D500E42796 /* LongPressButton.swift in Sources */, @@ -12027,6 +12042,7 @@ 4B2F565D2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 4B957B082AC7AE700062CA31 /* PixelDataStore.swift in Sources */, 4B957B092AC7AE700062CA31 /* WaitlistStorage.swift in Sources */, + B6ABD0D02BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */, 4B957B0A2AC7AE700062CA31 /* Pixel.swift in Sources */, 4B957B0B2AC7AE700062CA31 /* PixelEvent.swift in Sources */, 4B957B0C2AC7AE700062CA31 /* TabBarFooter.swift in Sources */, @@ -12051,6 +12067,7 @@ F1B33DF42BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */, 4B957B1B2AC7AE700062CA31 /* ScriptSourceProviding.swift in Sources */, 4B957B1C2AC7AE700062CA31 /* CoreDataBookmarkImporter.swift in Sources */, + B6ABD0CC2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, 4B957B1D2AC7AE700062CA31 /* SuggestionViewModel.swift in Sources */, 4B957B1E2AC7AE700062CA31 /* BookmarkManagedObject.swift in Sources */, 4B957B1F2AC7AE700062CA31 /* CSVLoginExporter.swift in Sources */, @@ -12895,6 +12912,7 @@ 4B37EE5F2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */, 4B9DB0472A983B24000927DB /* WaitlistRootView.swift in Sources */, 31F28C5128C8EEC500119F70 /* YoutubeOverlayUserScript.swift in Sources */, + B6ABD0CA2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */, B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, B6DA06E62913F39400225DE2 /* MenuItemSelectors.swift in Sources */, @@ -13016,6 +13034,7 @@ B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */, 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */, 1D36F4242A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, + B6ABD0CE2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */, 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, 85AC3AEF25D5CE9800C7D2AA /* UserScripts.swift in Sources */, B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */, @@ -13378,7 +13397,10 @@ B6E6BA062BA1FE10008AA7E1 /* NSApplicationExtension.swift in Sources */, B6E6B9F62BA1FD90008AA7E1 /* SandboxTestTool.swift in Sources */, B6E6BA252BA2EDDE008AA7E1 /* FileReadResult.swift in Sources */, + B6EECB322BC40A1400B3CB77 /* FileManagerExtension.swift in Sources */, + B6EECB302BC3FA5A00B3CB77 /* SecurityScopedFileURLController.swift in Sources */, B6E6BA052BA1FE09008AA7E1 /* URLExtension.swift in Sources */, + B6EECB312BC3FAB100B3CB77 /* NSURL+sandboxExtensionRetainCount.m in Sources */, B6E6BA202BA2E462008AA7E1 /* CollectionExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index c524d39c8c..b7e767fece 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -101,4 +101,16 @@ extension FileManager { return resolvedUrl.path.hasPrefix(trashUrl.path) } + /// Check if location pointed by the URL is writable by writing an empty data to it and removing the file if write succeeds + /// - Throws error if writing to the location fails + func checkWritability(_ url: URL) throws { + if fileExists(atPath: url.path), isWritableFile(atPath: url.path) { + return // we can write + } else { + // either we can‘t write or there‘s no file at the url – try writing throwing access error if no permission + try Data().write(to: url) + try removeItem(at: url) + } + } + } diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 6ea129d2fb..b79720a9d5 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -469,6 +469,50 @@ extension URL { } + var isFileHidden: Bool { + get throws { + try self.resourceValues(forKeys: [.isHiddenKey]).isHidden ?? false + } + } + + mutating func setFileHidden(_ hidden: Bool) throws { + var resourceValues = URLResourceValues() + resourceValues.isHidden = true + try setResourceValues(resourceValues) + } + + /// Check if location pointed by the URL is writable + /// - Note: if there‘s no file at the URL, it will try to create a file and then remove it + func isWritableLocation() -> Bool { + do { + try FileManager.default.checkWritability(self) + return true + } catch { + return false + } + } + +#if DEBUG && APPSTORE + /// sandbox extension URL access should be stopped after SecurityScopedFileURLController is deallocated - this function validates it and breaks if the file is still writable + func ensureUrlIsNotWritable(or handler: () -> Void) { + let fm = FileManager.default + // is the URL ~/Downloads? + if self.resolvingSymlinksInPath() == fm.urls(for: .downloadsDirectory, in: .userDomainMask).first!.resolvingSymlinksInPath() { + assert(isWritableLocation()) + return + } + // is parent directory writable (e.g. ~/Downloads)? + if fm.isWritableFile(atPath: self.deletingLastPathComponent().path) + // trashed files are still accessible for some reason even after stopping access + || fm.isInTrash(self) + // other file is being saved at the same URL + || NSURL.activeSecurityScopedUrlUsages.contains(where: { $0.url !== self as NSURL && $0.url == self as NSURL }) + || !isWritableLocation() { return } + + handler() + } +#endif + // MARK: - System Settings static var fullDiskAccess = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")! diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift index de04f91cb7..3740e0e223 100644 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ b/DuckDuckGo/Common/Logging/Logging.swift @@ -140,3 +140,45 @@ func logOrAssertionFailure(_ message: String) { os_log("%{public}s", type: .error, message) #endif } + +#if DEBUG + +func breakByRaisingSigInt(_ description: String, file: StaticString = #file, line: Int = #line) { + let fileLine = "\(("\(file)" as NSString).lastPathComponent):\(line)" + os_log(""" + + + ------------------------------------------------------------------------------------------------------ + BREAK at %s: + ------------------------------------------------------------------------------------------------------ + + %s + + Hit Continue (^⌘Y) to continue program execution + ------------------------------------------------------------------------------------------------------ + + """, type: .debug, fileLine, description.components(separatedBy: "\n").map { " " + $0.trimmingWhitespace() }.joined(separator: "\n")) + raise(SIGINT) +} + +// get symbol from stack trace for a caller of a calling method +func callingSymbol() -> String { + let stackTrace = Thread.callStackSymbols + // find `callingSymbol` itself or dispatch_once_callout + var callingSymbolIdx = stackTrace.firstIndex(where: { $0.contains("_dispatch_once_callout") }) + ?? stackTrace.firstIndex(where: { $0.contains("callingSymbol") })! + // procedure calling `callingSymbol` + callingSymbolIdx += 1 + + var symbolName: String + repeat { + // caller for the procedure + callingSymbolIdx += 1 + let line = stackTrace[callingSymbolIdx].replacingOccurrences(of: Bundle.main.executableURL!.lastPathComponent, with: "DDG") + symbolName = String(line.split(separator: " ", maxSplits: 3)[3]).components(separatedBy: " + ")[0] + } while stackTrace[callingSymbolIdx - 1].contains(symbolName.dropping(suffix: "To")) // skip objc wrappers + + return symbolName +} + +#endif diff --git a/DuckDuckGo/FileDownload/Model/FilePresenter.swift b/DuckDuckGo/FileDownload/Model/FilePresenter.swift index be8d94e8a6..1770935af2 100644 --- a/DuckDuckGo/FileDownload/Model/FilePresenter.swift +++ b/DuckDuckGo/FileDownload/Model/FilePresenter.swift @@ -23,7 +23,6 @@ import Foundation private protocol FilePresenterDelegate: AnyObject { var logger: FilePresenterLogger { get } var url: URL? { get } - var primaryPresentedItemURL: URL? { get } func presentedItemDidMove(to newURL: URL) func accommodatePresentedItemDeletion() throws func accommodatePresentedItemEviction() throws @@ -57,12 +56,14 @@ internal class FilePresenter { final let presentedItemOperationQueue: OperationQueue fileprivate final weak var delegate: FilePresenterDelegate? - init(presentedItemOperationQueue: OperationQueue) { + init(presentedItemOperationQueue: OperationQueue, delegate: FilePresenterDelegate) { self.presentedItemOperationQueue = presentedItemOperationQueue + self.delegate = delegate } + final var fallbackPresentedItemURL: URL? final var presentedItemURL: URL? { - guard let delegate else { return nil } + guard let delegate else { return fallbackPresentedItemURL } FilePresenter.dispatchSourceQueue.async { // prevent owning FilePresenter deallocation inside the presentedItemURL getter withExtendedLifetime(delegate) {} @@ -72,12 +73,10 @@ internal class FilePresenter { } final func presentedItemDidMove(to newURL: URL) { - assert(delegate != nil) delegate?.presentedItemDidMove(to: newURL) } func accommodatePresentedItemDeletion(completionHandler: @escaping @Sendable ((any Error)?) -> Void) { - assert(delegate != nil) do { try delegate?.accommodatePresentedItemDeletion() completionHandler(nil) @@ -87,9 +86,8 @@ internal class FilePresenter { } func accommodatePresentedItemEviction(completionHandler: @escaping @Sendable ((any Error)?) -> Void) { - assert(delegate != nil) do { - try delegate?.accommodatePresentedItemEviction() + try delegate?.accommodatePresentedItemEviction() completionHandler(nil) } catch { completionHandler(error) @@ -100,74 +98,138 @@ internal class FilePresenter { final private class DelegatingRelatedFilePresenter: DelegatingFilePresenter { - var primaryPresentedItemURL: URL? { - let url = delegate?.primaryPresentedItemURL - return url + let primaryPresentedItemURL: URL? + + init(primaryPresentedItemURL: URL?, presentedItemOperationQueue: OperationQueue, delegate: FilePresenterDelegate) { + self.primaryPresentedItemURL = primaryPresentedItemURL + super.init(presentedItemOperationQueue: presentedItemOperationQueue, delegate: delegate) } } fileprivate let lock = NSLock() - private var innerPresenter: DelegatingFilePresenter? + private var innerPresenters = [DelegatingFilePresenter]() private var dispatchSourceCancellable: AnyCancellable? fileprivate let logger: any FilePresenterLogger - let primaryPresentedItemURL: URL? - - private var _url: URL? + private var urlController: SecurityScopedFileURLController? final var url: URL? { lock.withLock { - _url + urlController?.url } } private func setURL(_ newURL: URL?) { - guard let oldValue = lock.withLock({ () -> URL?? in - let oldValue = _url - guard oldValue != newURL else { return URL??.none } - _url = newURL - return oldValue - }) else { return } + guard let oldValue = lock.withLock({ _setURL(newURL) }) else { return } didSetURL(newURL, oldValue: oldValue) } + // inside locked scope + private func _setURL(_ newURL: URL?) -> URL?? /* returns old value (URL?) if new value was updated */ { + let oldValue = urlController?.url + guard oldValue != newURL else { return URL??.none } + guard let newURL else { + urlController = nil + return newURL + } + + // if the new url is pointing to the same path (only letter case has changed) - keep its sandbox extension in a new Controller + if let urlController, let oldValue, + oldValue.resolvingSymlinksInPath().path == newURL.resolvingSymlinksInPath().path, + urlController.isManagingSecurityScope { + urlController.updateUrlKeepingSandboxExtensionRetainCount(newURL) + } else { + urlController = SecurityScopedFileURLController(url: newURL, logger: logger) + } + + return oldValue + } + private var urlSubject = PassthroughSubject() final var urlPublisher: AnyPublisher { urlSubject.prepend(url).eraseToAnyPublisher() } - init(url: URL, primaryItemURL: URL? = nil, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { - self._url = url - self.primaryPresentedItemURL = primaryItemURL + /// - Parameter url: represented file URL access to which is coordinated by the File Presenter. + /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). + /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. + /// - Note: see https://stackoverflow.com/questions/25627628/sandboxed-mac-app-exhausting-security-scoped-url-resources + init(url: URL, consumeUnbalancedStartAccessingResource: Bool = false, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + self.urlController = SecurityScopedFileURLController(url: url, manageSecurityScope: consumeUnbalancedStartAccessingResource, logger: logger) + self.logger = logger - let innerPresenter: DelegatingFilePresenter - if primaryItemURL != nil { - innerPresenter = DelegatingRelatedFilePresenter(presentedItemOperationQueue: FilePresenter.presentedItemOperationQueue) + do { + try setupInnerPresenter(for: url, primaryItemURL: nil, createIfNeededCallback: createIfNeededCallback) + logger.log("🗄️ added file presenter for \"\(url.path)\"") + } catch { + removeFilePresenters() + throw error + } + } + + /// - Parameter url: represented file URL access to which is coordinated by the File Presenter. + /// - Parameter primaryItemURL: URL to a main file resource access to which has been granted. + /// Used to grant out-of-sandbox access to `url` representing a “related” resource like “download.duckload” where the `primaryItemURL` would point to “download.zip”. + /// - Note: the related (“duckload”) file extension should be registered in the Info.plist with `NSIsRelatedItemType` flag set to `true`. + /// - Note: when presenting a related item the security scoped resource access will always be stopped on the File Presenter deallocation + /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). + /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. + init(url: URL, primaryItemURL: URL, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + self.urlController = SecurityScopedFileURLController(url: url, logger: logger) + self.logger = logger + + do { + try setupInnerPresenter(for: url, primaryItemURL: primaryItemURL, createIfNeededCallback: createIfNeededCallback) + logger.log("🗄️ added file presenter for \"\(url.path) primary item: \"\(primaryItemURL.path)\"") + } catch { + removeFilePresenters() + throw error + } + } + + private func setupInnerPresenter(for url: URL, primaryItemURL: URL?, createIfNeededCallback: ((URL) throws -> URL)?) throws { + let innerPresenter = if let primaryItemURL { + DelegatingRelatedFilePresenter(primaryPresentedItemURL: primaryItemURL, presentedItemOperationQueue: FilePresenter.presentedItemOperationQueue, delegate: self) } else { - innerPresenter = DelegatingFilePresenter(presentedItemOperationQueue: FilePresenter.presentedItemOperationQueue) + DelegatingFilePresenter(presentedItemOperationQueue: FilePresenter.presentedItemOperationQueue, delegate: self) } - self.innerPresenter = innerPresenter - innerPresenter.delegate = self + self.innerPresenters = [innerPresenter] + NSFileCoordinator.addFilePresenter(innerPresenter) if !FileManager.default.fileExists(atPath: url.path) { - if let createFile = createIfNeededCallback { - logger.log("🗄️💥 creating file for presenter at \"\(url.path)\"") - self._url = try coordinateWrite(at: url, using: createFile) - - // re-add File Presenter for the updated URL + guard let createFile = createIfNeededCallback else { + throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path]) + } + logger.log("🗄️💥 creating file for presenter at \"\(url.path)\"") + // create new file at the presented URL using the provided callback and update URL if needed + _=self._setURL( + try coordinateWrite(at: url, using: createFile) + ) + + if primaryItemURL == nil { + // Remove and re-add the file presenter for regular item presenters. NSFileCoordinator.removeFilePresenter(innerPresenter) NSFileCoordinator.addFilePresenter(innerPresenter) - - } else { - throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path]) } } - addFSODispatchSource(for: url) + // to correctly handle file move events for a “related” item presenters we need to use a secondary presenter + if primaryItemURL != nil { + // set permanent original url without tracking file movements + // to correctly release the sandbox extension when the “related” presenter is removed + innerPresenter.fallbackPresentedItemURL = url + innerPresenter.delegate = nil + + let innerPresenter2 = DelegatingFilePresenter(presentedItemOperationQueue: FilePresenter.presentedItemOperationQueue, delegate: self) + NSFileCoordinator.addFilePresenter(innerPresenter2) + innerPresenters.append(innerPresenter2) + } - logger.log("🗄️ added file presenter for \"\(url.path)\"\(primaryPresentedItemURL != nil ? " primary item: \"\(primaryPresentedItemURL!.path)\"" : "")") + try coordinateRead(at: url, with: .withoutChanges) { url in + addFSODispatchSource(for: url) + } } private func addFSODispatchSource(for url: URL) { @@ -187,7 +249,7 @@ internal class FilePresenter { self.logger.log("🗄️⚠️ file delete event handler: \"\(url.path)\"") var resolvedBookmarkData: URL? { var isStale = false - guard let presenter = self as? SandboxFilePresenter, + guard let presenter = self as? BookmarkFilePresenter, let bookmarkData = presenter.fileBookmarkData, let url = try? URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &isStale) else { if FileManager().fileExists(atPath: url.path) { return url } // file still exists but with different letter case ? @@ -212,25 +274,36 @@ internal class FilePresenter { dispatchSource.resume() } - private func removeFilePresenter() { - if let innerPresenter { - logger.log("🗄️ removing file presenter for \"\(url?.path ?? "")\"") + private func removeFilePresenters() { + for (idx, innerPresenter) in innerPresenters.enumerated() { + // innerPresenter delegate won‘t be available at this point when called from `deinit`, + // so set the final url here to correctly remove the presenter. + if innerPresenter.fallbackPresentedItemURL == nil { + innerPresenter.fallbackPresentedItemURL = urlController?.url + } + logger.log("🗄️ removing file presenter \(idx) for \"\(innerPresenter.presentedItemURL?.path ?? "")\"") NSFileCoordinator.removeFilePresenter(innerPresenter) - self.innerPresenter = nil } + if innerPresenters.count > 1 { + // ”related” item File Presenters make an unbalanced sandbox extension retain, + // release the actual file URL sandbox extension by calling an extra `stopAccessingSecurityScopedResource` + urlController?.url.consumeUnbalancedStartAccessingSecurityScopedResource() + } + innerPresenters = [] } fileprivate func didSetURL(_ newValue: URL?, oldValue: URL?) { - assert(newValue != oldValue) + assert(newValue == nil || newValue != oldValue) logger.log("🗄️ did update url from \"\(oldValue?.path ?? "")\" to \"\(newValue?.path ?? "")\"") urlSubject.send(newValue) } deinit { - removeFilePresenter() + removeFilePresenters() } } + extension FilePresenter: FilePresenterDelegate { func presentedItemDidMove(to newURL: URL) { @@ -240,8 +313,9 @@ extension FilePresenter: FilePresenterDelegate { func accommodatePresentedItemDeletion() throws { logger.log("🗄️ accommodatePresentedItemDeletion (\"\(url?.path ?? "")\")") + // should go before resetting the URL to correctly remove File Presenter + removeFilePresenters() setURL(nil) - removeFilePresenter() } func accommodatePresentedItemEviction() throws { @@ -254,9 +328,7 @@ extension FilePresenter: FilePresenterDelegate { /// Maintains File Bookmark Data for presented resource URL /// and manages its sandbox security scope access calling `stopAccessingSecurityScopedResource` on deinit /// balanced with preceding `startAccessingSecurityScopedResource` -final class SandboxFilePresenter: FilePresenter { - - private let securityScopedURL: URL? +final class BookmarkFilePresenter: FilePresenter { private var _fileBookmarkData: Data? final var fileBookmarkData: Data? { @@ -271,21 +343,31 @@ final class SandboxFilePresenter: FilePresenter { } /// - Parameter url: represented file URL access to which is coordinated by the File Presenter. - /// - Parameter primaryItemURL: URL to a main file resource access to which has been granted. - /// Used to grant out-of-sandbox access to `url` representing a “secondary” resource like “download.duckload” where the `primaryItemURL` would point to “download.zip”. - /// - Note: the secondary (“duckload”) file extension should be registered in the Info.plist with `NSIsRelatedItemType` flag set to `true`. /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. - init(url: URL, primaryItemURL: URL? = nil, consumeUnbalancedStartAccessingResource: Bool = false, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + override init(url: URL, consumeUnbalancedStartAccessingResource: Bool = false, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { - if consumeUnbalancedStartAccessingResource || url.startAccessingSecurityScopedResource() == true { - self.securityScopedURL = url - logger.log("🏝️ \(consumeUnbalancedStartAccessingResource ? "consuming unbalanced startAccessingResource for" : "started resource access for") \"\(url.path)\"") - } else { - self.securityScopedURL = nil - logger.log("🏖️ didn‘t start resource access for \"\(url.path)\"") + try super.init(url: url, consumeUnbalancedStartAccessingResource: consumeUnbalancedStartAccessingResource, logger: logger, createIfNeededCallback: createIfNeededCallback) + + do { + try self.coordinateRead(at: url, with: .withoutChanges) { url in + logger.log("📒 updating bookmark data for \"\(url.path)\"") + self._fileBookmarkData = try url.bookmarkData(options: .withSecurityScope) + } + } catch { + logger.log("📕 bookmark data retreival failed for \"\(url.path)\": \(error)") + throw error } + } + /// - Parameter url: represented file URL access to which is coordinated by the File Presenter. + /// - Parameter primaryItemURL: URL to a main file resource access to which has been granted. + /// Used to grant out-of-sandbox access to `url` representing a “related” resource like “download.duckload” where the `primaryItemURL` would point to “download.zip”. + /// - Note: the related (“duckload”) file extension should be registered in the Info.plist with `NSIsRelatedItemType` flag set to `true`. + /// - Note: when presenting a related item the security scoped resource access will always be stopped on the File Presenter deallocation + /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). + /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. + override init(url: URL, primaryItemURL: URL, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { try super.init(url: url, primaryItemURL: primaryItemURL, logger: logger, createIfNeededCallback: createIfNeededCallback) do { @@ -305,15 +387,8 @@ final class SandboxFilePresenter: FilePresenter { var isStale = false logger.log("📒 resolving url from bookmark data") let url = try URL(resolvingBookmarkData: fileBookmarkData, options: .withSecurityScope, bookmarkDataIsStale: &isStale) - if url.startAccessingSecurityScopedResource() == true { - self.securityScopedURL = url - logger.log("🏝️ started resource access for \"\(url.path)\"\(isStale ? " (stale)" : "")") - } else { - self.securityScopedURL = nil - logger.log("🏖️ didn‘t start resource access for \"\(url.path)\"\(isStale ? " (stale)" : "")") - } - try super.init(url: url, logger: logger) + try super.init(url: url, consumeUnbalancedStartAccessingResource: true, logger: logger) if isStale { DispatchQueue.global().async { [weak self] in @@ -346,25 +421,18 @@ final class SandboxFilePresenter: FilePresenter { fileBookmarkDataSubject.send(fileBookmarkData) } - deinit { - if let securityScopedURL { - logger.log("🗄️ stopAccessingSecurityScopedResource \"\(securityScopedURL.path)\"") - securityScopedURL.stopAccessingSecurityScopedResource() - } - } - } extension FilePresenter { func coordinateRead(at url: URL? = nil, with options: NSFileCoordinator.ReadingOptions = [], using reader: (URL) throws -> T) throws -> T { - guard let innerPresenter, let url = url ?? self.url else { throw CocoaError(.fileNoSuchFile) } + guard let innerPresenter = innerPresenters.last, let url = url ?? self.url else { throw CocoaError(.fileNoSuchFile) } return try NSFileCoordinator(filePresenter: innerPresenter).coordinateRead(at: url, with: options, using: reader) } func coordinateWrite(at url: URL? = nil, with options: NSFileCoordinator.WritingOptions = [], using writer: (URL) throws -> T) throws -> T { - guard let innerPresenter, let url = url ?? self.url else { throw CocoaError(.fileNoSuchFile) } + guard let innerPresenter = innerPresenters.last, let url = url ?? self.url else { throw CocoaError(.fileNoSuchFile) } // temporarily disable DispatchSource file removal events dispatchSourceCancellable?.cancel() @@ -377,7 +445,7 @@ extension FilePresenter { } public func coordinateMove(from url: URL? = nil, to: URL, with options2: NSFileCoordinator.WritingOptions = .forReplacing, using move: (URL, URL) throws -> T) throws -> T { - guard let innerPresenter, let url = url ?? self.url else { throw CocoaError(.fileNoSuchFile) } + guard let innerPresenter = innerPresenters.last, let url = url ?? self.url else { throw CocoaError(.fileNoSuchFile) } return try NSFileCoordinator(filePresenter: innerPresenter).coordinateMove(from: url, to: to, with: options2, using: move) } @@ -425,33 +493,3 @@ extension NSFileCoordinator { } } - -#if DEBUG -extension NSURL { - - private static var stopAccessingSecurityScopedResourceCallback: ((URL) -> Void)? - - private static let originalStopAccessingSecurityScopedResource = { - class_getInstanceMethod(NSURL.self, #selector(NSURL.stopAccessingSecurityScopedResource))! - }() - private static let swizzledStopAccessingSecurityScopedResource = { - class_getInstanceMethod(NSURL.self, #selector(NSURL.swizzled_stopAccessingSecurityScopedResource))! - }() - private static let swizzleStopAccessingSecurityScopedResourceOnce: Void = { - method_exchangeImplementations(originalStopAccessingSecurityScopedResource, swizzledStopAccessingSecurityScopedResource) - }() - - static func swizzleStopAccessingSecurityScopedResource(with stopAccessingSecurityScopedResourceCallback: ((URL) -> Void)?) { - _=swizzleStopAccessingSecurityScopedResourceOnce - self.stopAccessingSecurityScopedResourceCallback = stopAccessingSecurityScopedResourceCallback - } - - @objc private dynamic func swizzled_stopAccessingSecurityScopedResource() { - if let stopAccessingSecurityScopedResourceCallback = Self.stopAccessingSecurityScopedResourceCallback { - stopAccessingSecurityScopedResourceCallback(self as URL) - } - self.swizzled_stopAccessingSecurityScopedResource() // call original - } - -} -#endif diff --git a/DuckDuckGo/FileDownload/Model/NSURL+sandboxExtensionRetainCount.m b/DuckDuckGo/FileDownload/Model/NSURL+sandboxExtensionRetainCount.m new file mode 100644 index 0000000000..8a312c2fb0 --- /dev/null +++ b/DuckDuckGo/FileDownload/Model/NSURL+sandboxExtensionRetainCount.m @@ -0,0 +1,40 @@ +// +// NSURL+sandboxExtensionRetainCount.m +// +// 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 + +// Macro for adding quotes +#define STRINGIFY(X) STRINGIFY2(X) +#define STRINGIFY2(X) #X + +#import STRINGIFY(SWIFT_OBJC_INTERFACE_HEADER_NAME) + +@implementation NSURL (sandboxExtensionRetainCount) + +/** + * This method will be automatically called at app launch time to swizzle `startAccessingSecurityScopedResource` and + * `stopAccessingSecurityScopedResource` methods to accurately reflect the current number of start and stop calls + * stored in the associated `NSURL.sandboxExtensionRetainCount` value. + * + * See SecurityScopedFileURLController.swift + */ ++ (void)initialize { + [self swizzleStartStopAccessingSecurityScopedResourceOnce]; +} + +@end diff --git a/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift b/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift new file mode 100644 index 0000000000..7cdc7eecdf --- /dev/null +++ b/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift @@ -0,0 +1,179 @@ +// +// SecurityScopedFileURLController.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 Common + +/// Manages security-scoped resource access to a file URL. +/// +/// This class is designed to consume unbalanced `startAccessingSecurityScopedResource` calls and ensure proper +/// resource cleanup by calling `stopAccessingSecurityScopedResource` the appropriate number of times +/// to end the resource access securely. +/// +/// - Note: Used in conjunction with NSURL extension swizzling the `startAccessingSecurityScopedResource` and +/// `stopAccessingSecurityScopedResource` methods to accurately reflect the current number of start and stop calls. +/// The number is reflected in the associated `URL.sandboxExtensionRetainCount` value. +final class SecurityScopedFileURLController { + + fileprivate let logger: any FilePresenterLogger + + private(set) var url: URL + let isManagingSecurityScope: Bool + + /// Initializes a new instance of `SecurityScopedFileURLController` with the provided URL and security-scoped resource handling options. + /// + /// - Parameters: + /// - url: The URL of the file to manage. + /// - manageSecurityScope: A Boolean value indicating whether the controller should manage the URL security scope access (i.e. call stop and end accessing resource methods). + /// - logger: An optional logger instance for logging file operations. Defaults to disabled. + /// - Note: when `manageSecurityScope` is `true` access to the represented URL will be stopped for the whole app on the controller deallocation. + init(url: URL, manageSecurityScope: Bool = true, logger: any FilePresenterLogger = OSLog.disabled) { + assert(url.isFileURL) +#if APPSTORE + let didStartAccess = manageSecurityScope && url.startAccessingSecurityScopedResource() +#else + let didStartAccess = false +#endif + self.url = url + self.isManagingSecurityScope = didStartAccess + self.logger = logger + logger.log("\(didStartAccess ? "🧪 " : "")SecurityScopedFileURLController.init: \(url.sandboxExtensionRetainCount) – \"\(url.path)\"") + } + + func updateUrlKeepingSandboxExtensionRetainCount(_ newURL: URL) { + guard newURL as NSURL !== url as NSURL else { return } + + for _ in 0.. Bool { + if self.swizzled_startAccessingSecurityScopedResource() /* call original */ { + sandboxExtensionRetainCount += 1 + return true + } + return false + } + + @objc private dynamic func swizzled_stopAccessingSecurityScopedResource() { + self.swizzled_stopAccessingSecurityScopedResource() // call original + + var sandboxExtensionRetainCount = self.sandboxExtensionRetainCount + if sandboxExtensionRetainCount > 0 { + sandboxExtensionRetainCount -= 1 + self.sandboxExtensionRetainCount = sandboxExtensionRetainCount + } + } + + private static let sandboxExtensionRetainCountKey = UnsafeRawPointer(bitPattern: "sandboxExtensionRetainCountKey".hashValue)! + fileprivate(set) var sandboxExtensionRetainCount: Int { + get { + (objc_getAssociatedObject(self, Self.sandboxExtensionRetainCountKey) as? NSNumber)?.intValue ?? 0 + } + set { + objc_setAssociatedObject(self, Self.sandboxExtensionRetainCountKey, NSNumber(value: newValue), .OBJC_ASSOCIATION_RETAIN) +#if DEBUG + if newValue > 0 { + NSURL.activeSecurityScopedUrlUsages.insert(.init(url: self)) + } else { + NSURL.activeSecurityScopedUrlUsages.remove(.init(url: self)) + } +#endif + } + } + +#if DEBUG + struct SecurityScopedUrlUsage: Hashable { + let url: NSURL + // hash url as object address + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(url)) + } + } + static var activeSecurityScopedUrlUsages: Set = [] +#endif + +} + +extension URL { + + /// The number of times the security-scoped resource associated with the URL has been accessed + /// using `startAccessingSecurityScopedResource` without a corresponding call to + /// `stopAccessingSecurityScopedResource`. This property provides a count of active accesses + /// to the security-scoped resource, helping manage resource cleanup and ensure proper + /// handling of security-scoped resources. + /// + /// - Note: Accessing this property requires NSURL extension swizzling of `startAccessingSecurityScopedResource` + /// and `stopAccessingSecurityScopedResource` methods to accurately track the count. + var sandboxExtensionRetainCount: Int { + (self as NSURL).sandboxExtensionRetainCount + } + + func consumeUnbalancedStartAccessingSecurityScopedResource() { + (self as NSURL).sandboxExtensionRetainCount += 1 + } + +} diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift index ca3d3e26c9..b263df5915 100644 --- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift +++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift @@ -116,6 +116,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable @MainActor private var itemReplacementDirectory: URL? @MainActor private var itemReplacementDirectoryFSOCancellable: AnyCancellable? @MainActor private var tempFileUrlCancellable: AnyCancellable? + @MainActor private(set) var selectedDestinationURL: URL? var originalRequest: URLRequest? { download.originalRequest @@ -226,6 +227,13 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable do { let fm = FileManager() guard let destinationURL else { throw URLError(.cancelled) } + // in case we‘re overwriting the URL – increment the access counter for the duration of the method + let accessStarted = destinationURL.startAccessingSecurityScopedResource() + defer { + if accessStarted { + destinationURL.stopAccessingSecurityScopedResource() + } + } os_log(.debug, log: log, "download task callback: creating temp directory for \"\(destinationURL.path)\"") switch cleanupStyle { @@ -333,15 +341,15 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable /// opens File Presenters for destination file and temp file private nonisolated func filePresenters(for destinationURL: URL, tempURL: URL) async throws -> (tempFile: FilePresenter, destinationFile: FilePresenter) { var destinationURL = destinationURL - let duckloadURL = destinationURL.deletingPathExtension().appendingPathExtension(Self.downloadExtension) - let fm = FileManager.default + var duckloadURL = destinationURL.deletingPathExtension().appendingPathExtension(Self.downloadExtension) + let fm = FileManager() // 🧙‍♂️ now we‘re doing do some magique here 🧙‍♂️ // -------------------------------------- os_log(.debug, log: log, "🧙‍♂️ magique.start: \"\(destinationURL.path)\" (\"\(duckloadURL.path)\") directory writable: \(fm.isWritableFile(atPath: destinationURL.deletingLastPathComponent().path))") // 1. create our final destination file (let‘s say myfile.zip) and setup a File Presenter for it // doing this we preserve access to the file until it‘s actually downloaded - let destinationFilePresenter = try SandboxFilePresenter(url: destinationURL, consumeUnbalancedStartAccessingResource: true, logger: log) { url in + let destinationFilePresenter = try BookmarkFilePresenter(url: destinationURL, consumeUnbalancedStartAccessingResource: true, logger: log) { url in try fm.createFile(atPath: url.path, contents: nil) ? url : { throw CocoaError(.fileWriteNoPermission, userInfo: [NSFilePathErrorKey: url.path]) }() @@ -353,45 +361,76 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable // 2. mark the file as hidden until it‘s downloaded to not to confuse user // and prevent from unintentional opening of the empty file - var resourceValues = URLResourceValues() - resourceValues.isHidden = true - try destinationURL.setResourceValues(resourceValues) - os_log(.debug, log: log, "🧙‍♂️ \"\(destinationURL.path)\" hidden, moving temp file from \"\(tempURL.path)\" to \"\(duckloadURL.path)\"") + try destinationURL.setFileHidden(true) + os_log(.debug, log: log, "🧙‍♂️ \"\(destinationURL.path)\" hidden") - // 3. then we move the temporary download file to the destination directory (myfile.zip.duckload) + // 3. then we move the temporary download file to the destination directory (myfile.duckload) // this is doable in sandboxed builds by using “Related Items” i.e. using a file URL with an extra // `.duckload` extension appended and “Primary Item” pointing to the sandbox-accessible destination URL // the `.duckload` document type is registered in the Info.plist with `NSIsRelatedItemType` flag // // - after the file is downloaded we‘ll replace the destination file with the `.duckload` file if fm.fileExists(atPath: duckloadURL.path) { - // remove the `.duckload` item if already exists + // `.duckload` already exists do { - try FilePresenter(url: duckloadURL, primaryItemURL: destinationURL).coordinateWrite(with: .forDeleting) { duckloadURL in - try fm.removeItem(at: duckloadURL) - } + try chooseAlternativeDuckloadFileNameOrRemove(&duckloadURL, destinationURL: destinationURL) } catch { // that‘s ok, we‘ll keep using the original temp file - os_log(.error, log: log, "❗️ could not remove \"\(duckloadURL.path)\" \(error)") + os_log(.error, log: log, "❗️ can‘t resolve duckload file exists: \"\(duckloadURL.path)\": \(error)") + duckloadURL = tempURL } } - // now move the temp file to `.duckload` instantiating a File Presenter with it - let tempFilePresenter = try SandboxFilePresenter(url: duckloadURL, primaryItemURL: destinationURL, logger: log) { [log] duckloadURL in - do { - try fm.moveItem(at: tempURL, to: duckloadURL) - } catch { - // fallback: move failed, keep the temp file in the original location - os_log(.error, log: log, "🙁 fallback with \(error), will use \(tempURL.path)") - Pixel.fire(.debug(event: .fileAccessRelatedItemFailed, error: error)) - return tempURL + + let tempFilePresenter = if duckloadURL == tempURL { + // we won‘t use a `.duckload` file for this download, the file will be left in the temp location instead + try BookmarkFilePresenter(url: duckloadURL, logger: log) + } else { + // now move the temp file to `.duckload` instantiating a File Presenter with it + try BookmarkFilePresenter(url: duckloadURL, primaryItemURL: destinationURL, logger: log) { [log] duckloadURL in + do { + try fm.moveItem(at: tempURL, to: duckloadURL) + return duckloadURL + } catch { + // fallback: move failed, keep the temp file in the original location + os_log(.error, log: log, "🙁 fallback with \(error), will use \(tempURL.path)") + Pixel.fire(.debug(event: .fileAccessRelatedItemFailed, error: error)) + return tempURL + } } - return duckloadURL } os_log(.debug, log: log, "🧙‍♂️ \"\(duckloadURL.path)\" (\"\(tempFilePresenter.url?.path ?? "")\") ready") return (tempFile: tempFilePresenter, destinationFile: destinationFilePresenter) } + private func chooseAlternativeDuckloadFileNameOrRemove(_ duckloadURL: inout URL, destinationURL: URL) throws { + let fm = FileManager() + // are we using the `.duckload` file for some other download (with different extension)? + if NSFileCoordinator.filePresenters.first(where: { $0.presentedItemURL?.resolvingSymlinksInPath() == duckloadURL.resolvingSymlinksInPath() }) != nil { + // if the downloads directory is writable without extra permission – try choosing another `.duckload` filename + if fm.isWritableFile(atPath: duckloadURL.deletingLastPathComponent().path) { + // append `.duckload` to the destination file name with extension + let destinationPathExtension = destinationURL.pathExtension + let pathExtension = destinationPathExtension.isEmpty ? Self.downloadExtension : destinationPathExtension + "." + Self.downloadExtension + duckloadURL = duckloadURL.deletingPathExtension().appendingPathExtension(pathExtension) + + // choose non-existent path + duckloadURL = try fm.withNonExistentUrl(for: duckloadURL, incrementingIndexIfExistsUpTo: 1000, pathExtension: pathExtension) { url in + try Data().write(to: url) + return url + } + } else { + // continue keeping the temp file in the temp dir + throw CocoaError(.fileWriteFileExists) + } + } + + os_log(.debug, log: log, "removing temp file \"\(duckloadURL.path)\"") + try FilePresenter(url: duckloadURL, primaryItemURL: destinationURL).coordinateWrite(with: .forDeleting) { duckloadURL in + try fm.removeItem(at: duckloadURL) + } + } + private nonisolated func reuseFilePresenters(tempFile: FilePresenter, destination: FilePresenter, tempURL: URL) async throws -> (tempFile: FilePresenter, destinationFile: FilePresenter) { // if the download is “resumed” as a new download (replacing the destination file) - // use the existing `.duckload` file and move the temp file in its place @@ -587,6 +626,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { return nil } + self.selectedDestinationURL = destinationURL return await prepareChosenDestinationURL(destinationURL, fileType: suggestedFileType, cleanupStyle: cleanupStyle) } @@ -697,20 +737,7 @@ extension WebKitDownloadTask { override var description: String { guard Thread.isMainThread else { #if DEBUG - os_log(""" - - - ------------------------------------------------------------------------------------------------------ - BREAK: - ------------------------------------------------------------------------------------------------------ - - ❗️accessing WebKitDownloadTask.description from non-main thread - - Hit Continue (^⌘Y) to continue program execution - ------------------------------------------------------------------------------------------------------ - - """, type: .fault) - raise(SIGINT) + breakByRaisingSigInt("❗️accessing WebKitDownloadTask.description from non-main thread") #endif return "" } diff --git a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift index 5e9c9e14c9..70cfda3f2d 100644 --- a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift +++ b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift @@ -152,9 +152,9 @@ final class DownloadListCoordinator { // locate destination file let destinationPresenterResult = Result { if let destinationFileBookmarkData = item.destinationFileBookmarkData { - try SandboxFilePresenter(fileBookmarkData: destinationFileBookmarkData, logger: log) + try BookmarkFilePresenter(fileBookmarkData: destinationFileBookmarkData, logger: log) } else if let destinationURL = item.destinationURL { - try SandboxFilePresenter(url: destinationURL, logger: log) + try BookmarkFilePresenter(url: destinationURL, logger: log) } else { nil } @@ -163,9 +163,9 @@ final class DownloadListCoordinator { // locate temp download file var tempFilePresenterResult = Result { if let tempFileBookmarkData = item.tempFileBookmarkData { - try SandboxFilePresenter(fileBookmarkData: tempFileBookmarkData, logger: log) + try BookmarkFilePresenter(fileBookmarkData: tempFileBookmarkData, logger: log) } else if let tempURL = item.tempURL { - try SandboxFilePresenter(url: tempURL, logger: log) + try BookmarkFilePresenter(url: tempURL, logger: log) } else { nil } @@ -223,10 +223,10 @@ final class DownloadListCoordinator { case .downloading(destination: let destination, tempFile: let tempFile): self.addItemIfNeededAndSubscribe(to: (destination, tempFile), for: item) case .downloaded(let destination): - let updatedItem = self.downloadTask(task, withId: item.identifier, completedWith: .finished) + let updatedItem = self.downloadTask(task, withOriginalItem: item, completedWith: .finished) self.subscribeToPresenters((destination: destination, tempFile: nil), of: updatedItem ?? item) case .failed(destination: let destination, tempFile: let tempFile, resumeData: _, error: let error): - let updatedItem = self.downloadTask(task, withId: item.identifier, completedWith: .failure(error)) + let updatedItem = self.downloadTask(task, withOriginalItem: item, completedWith: .failure(error)) self.subscribeToPresenters((destination: destination, tempFile: tempFile), of: updatedItem ?? item) } } @@ -250,7 +250,7 @@ final class DownloadListCoordinator { Publishers.CombineLatest( presenters.destination?.urlPublisher ?? Just(nil).eraseToAnyPublisher(), - (presenters.destination as? SandboxFilePresenter)?.fileBookmarkDataPublisher ?? Just(nil).eraseToAnyPublisher() + (presenters.destination as? BookmarkFilePresenter)?.fileBookmarkDataPublisher ?? Just(nil).eraseToAnyPublisher() ) .scan((oldURL: nil, newURL: nil, fileBookmarkData: nil)) { (oldURL: $0.newURL, newURL: $1.0, fileBookmarkData: $1.1) } .sink { [weak self] oldURL, newURL, fileBookmarkData in @@ -279,7 +279,7 @@ final class DownloadListCoordinator { Publishers.CombineLatest( presenters.tempFile?.urlPublisher ?? Just(nil).eraseToAnyPublisher(), - (presenters.tempFile as? SandboxFilePresenter)?.fileBookmarkDataPublisher ?? Just(nil).eraseToAnyPublisher() + (presenters.tempFile as? BookmarkFilePresenter)?.fileBookmarkDataPublisher ?? Just(nil).eraseToAnyPublisher() ) .scan((oldURL: nil, newURL: nil, fileBookmarkData: nil)) { (oldURL: $0.newURL, newURL: $1.0, fileBookmarkData: $1.1) } .sink { [weak self] oldURL, newURL, fileBookmarkData in @@ -341,19 +341,25 @@ final class DownloadListCoordinator { } @MainActor - private func downloadTask(_ task: WebKitDownloadTask, withId identifier: UUID, completedWith result: Subscribers.Completion) -> DownloadListItem? { - os_log(.debug, log: log, "coordinator: task did finish \(identifier) \(task) with .\(result)") + private func downloadTask(_ task: WebKitDownloadTask, withOriginalItem initialItem: DownloadListItem, completedWith result: Subscribers.Completion) -> DownloadListItem? { + os_log(.debug, log: log, "coordinator: task did finish \(initialItem.identifier) \(task) with .\(result)") self.downloadTaskCancellables[task] = nil - // item will be really updated (completed) only if it was added before in `addItemOrUpdateFilePresenter` (when state switched to .downloading) - // if it has failed without starting - it won‘t be added or updated here - return updateItem(withId: identifier) { item in + return updateItem(withId: initialItem.identifier) { item in if item?.isBurner ?? false { item = nil return } + if item == nil, + case .failure(let failure) = result, !failure.isCancelled, + let fileName = task.selectedDestinationURL?.lastPathComponent { + // add instantly failed downloads to the list (not user-cancelled) + item = initialItem + item?.fileName = fileName + } + item?.progress = nil if case .failure(let error) = result { item?.error = error diff --git a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift index ce6962e4bf..e8e435c447 100644 --- a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Common import Foundation protocol DownloadsPreferencesPersistor { @@ -65,17 +66,19 @@ final class DownloadsPreferences: ObservableObject { static let shared = DownloadsPreferences(persistor: DownloadsPreferencesUserDefaultsPersistor()) - private func validatedDownloadLocation(_ location: String?) -> URL? { - if let selectedLocation = location, - let selectedLocationURL = URL(string: selectedLocation), - Self.isDownloadLocationValid(selectedLocationURL) { - return selectedLocationURL + private func validatedDownloadLocation(_ selectedLocation: URL?) -> URL? { + if let selectedLocation, Self.isDownloadLocationValid(selectedLocation) { + return selectedLocation } return nil } var effectiveDownloadLocation: URL? { - if let selectedLocationURL = alwaysRequestDownloadLocation ? validatedDownloadLocation(persistor.lastUsedCustomDownloadLocation) : validatedDownloadLocation(persistor.selectedDownloadLocation) { + if alwaysRequestDownloadLocation { + if let lastUsedCustomDownloadLocation = validatedDownloadLocation(persistor.lastUsedCustomDownloadLocation.flatMap(URL.init(string:))) { + return lastUsedCustomDownloadLocation + } + } else if let selectedLocationURL = validatedDownloadLocation(selectedDownloadLocation) { return selectedLocationURL } return Self.defaultDownloadLocation() @@ -94,37 +97,54 @@ final class DownloadsPreferences: ObservableObject { defer { objectWillChange.send() } - guard let newDownloadLocation = newValue else { - persistor.lastUsedCustomDownloadLocation = nil - return - } - if Self.isDownloadLocationValid(newDownloadLocation) { - persistor.lastUsedCustomDownloadLocation = newDownloadLocation.absoluteString - } + persistor.lastUsedCustomDownloadLocation = newValue?.absoluteString } } + private var selectedDownloadLocationController: SecurityScopedFileURLController? + var selectedDownloadLocation: URL? { get { - persistor.selectedDownloadLocation?.url + if let selectedDownloadLocation = selectedDownloadLocationController?.url { + return selectedDownloadLocation + } +#if APPSTORE + var isStale = false + if let bookmarkData = persistor.selectedDownloadLocation.flatMap({ Data(base64Encoded: $0) }), + let url = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) { + if isStale { + setSelectedDownloadLocation(url) // update bookmark data and selectedDownloadLocationController + } else { + selectedDownloadLocationController = SecurityScopedFileURLController(url: url, logger: OSLog.downloads) + } + return url + } +#endif + guard let url = persistor.selectedDownloadLocation.flatMap(URL.init(string:)), + url.isFileURL else { return nil } + return url.resolvingSymlinksInPath() } - set { defer { objectWillChange.send() } - guard let newDownloadLocation = newValue else { - persistor.selectedDownloadLocation = nil - return - } - if Self.isDownloadLocationValid(newDownloadLocation) { - persistor.selectedDownloadLocation = newDownloadLocation.absoluteString - } + setSelectedDownloadLocation(validatedDownloadLocation(newValue)) } } + private func setSelectedDownloadLocation(_ url: URL?) { + selectedDownloadLocationController = url.map { SecurityScopedFileURLController(url: $0, logger: OSLog.downloads) } + let locationString: String? +#if APPSTORE + locationString = (try? url?.bookmarkData(options: .withSecurityScope).base64EncodedString()) ?? url?.absoluteString +#else + locationString = url?.absoluteString +#endif + persistor.selectedDownloadLocation = locationString + } + var alwaysRequestDownloadLocation: Bool { get { persistor.alwaysRequestDownloadLocation diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 521893ba2e..dc4da53438 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -108,15 +108,15 @@ extension Preferences { // MARK: Location PreferencePaneSubSection { Text(UserText.downloadsLocation).bold() + HStack { NSPathControlView(url: downloadsModel.selectedDownloadLocation) -#if !APPSTORE Button(UserText.downloadsChangeDirectory) { downloadsModel.presentDownloadDirectoryPanel() } -#endif } .disabled(downloadsModel.alwaysRequestDownloadLocation) + ToggleMenuItem(UserText.downloadsAlwaysAsk, isOn: $downloadsModel.alwaysRequestDownloadLocation) } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 539d7007ea..d43aa95fd9 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -974,13 +974,28 @@ extension BrowserTabViewController: TabDelegate { suggestedFilename: request.parameters.suggestedFilename, directoryURL: directoryURL) - savePanel.beginSheetModal(for: window) { [weak request] response in + savePanel.beginSheetModal(for: window) { [weak request, weak self] response in switch response { case .abort: // panel not closed by user but by a tab switching return case .OK: - guard let url = savePanel.url else { fallthrough } + guard let self, + let window = view.window, + let url = savePanel.url else { fallthrough } + + do { + // validate selected URL is writable + try FileManager.default.checkWritability(url) + } catch { + // hide the save panel + self.activeUserDialogCancellable = nil + NSAlert(error: error).beginSheetModal(for: window) { [weak self] _ in + guard let self, let request else { return } + self.activeUserDialogCancellable = showSavePanel(with: request) + } + return + } request?.submit( (url, savePanel.selectedFileType) ) default: request?.submit(nil) diff --git a/DuckDuckGo/Tab/View/WebView.swift b/DuckDuckGo/Tab/View/WebView.swift index bc578f6d19..249356909b 100644 --- a/DuckDuckGo/Tab/View/WebView.swift +++ b/DuckDuckGo/Tab/View/WebView.swift @@ -30,6 +30,7 @@ protocol WebViewInteractionEventsDelegate: AnyObject { func webView(_ webView: WebView, scrollWheel event: NSEvent) } +@objc(DuckDuckGo_WebView) final class WebView: WKWebView { weak var contextMenuDelegate: WebViewContextMenuDelegate? diff --git a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift index 35e2024c05..d108921c11 100644 --- a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift +++ b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift @@ -70,9 +70,9 @@ class DownloadsIntegrationTests: XCTestCase { @MainActor func testWhenShouldDownloadResponse_downloadStarts() async throws { - let persistor = DownloadsPreferencesUserDefaultsPersistor() - persistor.alwaysRequestDownloadLocation = false - persistor.selectedDownloadLocation = FileManager.default.temporaryDirectory.absoluteString + let preferences = DownloadsPreferences.shared + preferences.alwaysRequestDownloadLocation = false + preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise() let suffix = Int.random(in: 0.. URL { @@ -95,15 +94,17 @@ final class FilePresenterTests: XCTestCase { return app } - private func terminateApp(timeout: TimeInterval = 1) async { - let eTerminated = runningApp != nil ? expectation(description: "terminated") : nil + private func terminateApp(timeout: TimeInterval = 5, expectation: XCTestExpectation = XCTestExpectation(description: "terminated")) async { + if runningApp == nil { + expectation.fulfill() + } let c = runningApp?.publisher(for: \.isTerminated).filter { $0 }.sink { _ in - eTerminated?.fulfill() + expectation.fulfill() } post(.terminate) runningApp?.forceTerminate() - await fulfillment(of: eTerminated.map { [$0] } ?? [], timeout: timeout) + await fulfillment(of: [expectation], timeout: timeout) withExtendedLifetime(c) {} } @@ -111,7 +112,7 @@ final class FilePresenterTests: XCTestCase { DistributedNotificationCenter.default().post(name: .init(name.rawValue), object: object) } - private func fileReadPromise(timeout: TimeInterval = 5) -> Future { + private func fileReadPromise(timeout: TimeInterval = 5, file: StaticString = #file, line: UInt = #line) -> Future { Future { [unowned self] fulfill in onFileRead = { result in fulfill(.success(result)) @@ -124,13 +125,14 @@ final class FilePresenterTests: XCTestCase { self.onError = nil } } - .timeout(timeout) + .timeout(timeout, file: file, line: line) .first() .promise() } // MARK: - Test sandboxed file access #if APPSTORE && !CI + func testTool_run() async throws { // 1. make non-sandbox file let nonSandboxUrl = try makeNonSandboxFile() @@ -177,7 +179,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) // 4. read the file @@ -188,7 +190,7 @@ final class FilePresenterTests: XCTestCase { XCTAssertEqual(result.data, testData.utf8String()) XCTAssertEqual(result.bookmark, bookmark) - // 5. close SandboxFilePresenter + // 5. close BookmarkFilePresenter let e = expectation(description: "access stopped") let c = DistributedNotificationCenter.default().publisher(for: SandboxTestNotification.stopAccessingSecurityScopedResourceCalled.name).sink { _ in e.fulfill() @@ -220,7 +222,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() post(.openFile, with: nonSandboxUrl.path) @@ -290,7 +292,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() @@ -316,7 +318,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() post(.openFile, with: nonSandboxUrl.path) @@ -355,7 +357,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() post(.openFile, with: nonSandboxUrl.path) @@ -374,7 +376,7 @@ final class FilePresenterTests: XCTestCase { } await fulfillment(of: [e], timeout: 5) - // 5. close SandboxFilePresenter + // 5. close BookmarkFilePresenter let eStopped = expectation(description: "access stopped") let c2 = DistributedNotificationCenter.default().publisher(for: SandboxTestNotification.stopAccessingSecurityScopedResourceCalled.name).sink { _ in eStopped.fulfill() @@ -406,13 +408,13 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() post(.openFile, with: nonSandboxUrl.path) _=try await fileReadPromise.value - // 4. close SandboxFilePresenter + // 4. close BookmarkFilePresenter let eStopped = expectation(description: "access stopped") let c2 = DistributedNotificationCenter.default().publisher(for: SandboxTestNotification.stopAccessingSecurityScopedResourceCalled.name).sink { _ in eStopped.fulfill() @@ -456,7 +458,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() post(.openFile, with: nonSandboxUrl.path) @@ -503,7 +505,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) fileReadPromise = self.fileReadPromise() post(.openFile, with: nonSandboxUrl.path) @@ -543,7 +545,7 @@ final class FilePresenterTests: XCTestCase { await terminateApp() runningApp = try await runHelperApp() - // 3. open the bookmark with SandboxFilePresenter + // 3. open the bookmark with BookmarkFilePresenter post(.openBookmarkWithFilePresenter, with: bookmark1.base64EncodedString()) post(.openBookmarkWithFilePresenter, with: bookmark2.base64EncodedString()) fileReadPromise = self.fileReadPromise() @@ -571,7 +573,6 @@ final class FilePresenterTests: XCTestCase { // 5. close FilePresenter 1 (at the original URL) let e3 = expectation(description: "access stopped") let c2 = DistributedNotificationCenter.default().publisher(for: SandboxTestNotification.stopAccessingSecurityScopedResourceCalled.name).sink { n in - XCTAssertEqual(n.object as? String, nonSandboxUrl1.path) e3.fulfill() } post(.closeFilePresenter, with: nonSandboxUrl1.path) @@ -600,32 +601,6 @@ final class FilePresenterTests: XCTestCase { } } - func testWhenFilePresenterClosesFileOpenedByOS_fileAccessIsPreserved() async throws { - // 1. make non-sandbox file and open the file with the helper app - let nonSandboxUrl = try makeNonSandboxFile() - var fileReadPromise = self.fileReadPromise() - runningApp = try await runHelperApp(opening: nonSandboxUrl) - guard let bookmark = try await fileReadPromise.value.bookmark else { XCTFail("No bookmark"); return } - - // 2. open file presenter - post(.openBookmarkWithFilePresenter, with: bookmark.base64EncodedString()) - - // 3. close the file presenter - let e = expectation(description: "access stopped") - let c = DistributedNotificationCenter.default().publisher(for: SandboxTestNotification.stopAccessingSecurityScopedResourceCalled.name).sink { n in - XCTAssertEqual(n.object as? String, nonSandboxUrl.path) - e.fulfill() - } - post(.closeFilePresenter, with: nonSandboxUrl.path) - await fulfillment(of: [e], timeout: 1) - withExtendedLifetime(c) {} - - // 4. validate file can still be read - fileReadPromise = self.fileReadPromise() - post(.openFile, with: nonSandboxUrl.path) - let result = try await fileReadPromise.value - XCTAssertEqual(result.path, nonSandboxUrl.path) - } #endif // MARK: - Test non-sandboxed file access @@ -633,10 +608,10 @@ final class FilePresenterTests: XCTestCase { func testWhenSandboxFilePresenterIsOpen_itCanReadFile_accessIsNotStoppedWhenClosed_noSandbox() async throws { // 1. make non-sandbox file; create bookmark let nonSandboxUrl = try makeNonSandboxFile() - guard let bookmarkData = try SandboxFilePresenter(url: nonSandboxUrl).fileBookmarkData else { XCTFail("No bookmark"); return } + guard let bookmarkData = try BookmarkFilePresenter(url: nonSandboxUrl).fileBookmarkData else { XCTFail("No bookmark"); return } - // 2. open the bookmark with SandboxFilePresenter - var filePresenter: SandboxFilePresenter! = try SandboxFilePresenter(fileBookmarkData: bookmarkData) + // 2. open the bookmark with BookmarkFilePresenter + var filePresenter: BookmarkFilePresenter! = try BookmarkFilePresenter(fileBookmarkData: bookmarkData) // 3. validate var publishedUrl: URL? @@ -656,7 +631,7 @@ final class FilePresenterTests: XCTestCase { func testWhenFileIsRenamed_urlIsUpdated_noSandbox() async throws { // 1. make non-sandbox file let nonSandboxUrl = try makeNonSandboxFile() - let filePresenter = try SandboxFilePresenter(url: nonSandboxUrl) + let filePresenter = try BookmarkFilePresenter(url: nonSandboxUrl) // 4. rename the file let newUrl = nonSandboxUrl.deletingPathExtension().appendingPathExtension("1.txt") @@ -689,7 +664,7 @@ final class FilePresenterTests: XCTestCase { func testWhenFileIsRemoved_removalIsDetected_noSandbox() async throws { // 1. make non-sandbox file let nonSandboxUrl = try makeNonSandboxFile() - let filePresenter = try SandboxFilePresenter(url: nonSandboxUrl) + let filePresenter = try BookmarkFilePresenter(url: nonSandboxUrl) // 2. remove the file let e1 = expectation(description: "file presenter: file removed") @@ -716,8 +691,8 @@ final class FilePresenterTests: XCTestCase { let bookmarkData1 = try nonSandboxUrl1.bookmarkData(options: .withSecurityScope) let nonSandboxUrl2 = try makeNonSandboxFile() let bookmarkData2 = try nonSandboxUrl2.bookmarkData(options: .withSecurityScope) - let filePresenter1 = try SandboxFilePresenter(fileBookmarkData: bookmarkData1) - let filePresenter2 = try SandboxFilePresenter(fileBookmarkData: bookmarkData2) + let filePresenter1 = try BookmarkFilePresenter(fileBookmarkData: bookmarkData1) + let filePresenter2 = try BookmarkFilePresenter(fileBookmarkData: bookmarkData2) // 2. cross-rename the files let tempUrl = nonSandboxUrl1.appendingPathExtension("tmp") diff --git a/UnitTests/Preferences/DownloadsPreferencesTests.swift b/UnitTests/Preferences/DownloadsPreferencesTests.swift index 3213d7c0af..7c50d88ff5 100644 --- a/UnitTests/Preferences/DownloadsPreferencesTests.swift +++ b/UnitTests/Preferences/DownloadsPreferencesTests.swift @@ -142,7 +142,7 @@ class DownloadsPreferencesTests: XCTestCase { preferences.selectedDownloadLocation = invalidDownloadLocationURL - XCTAssertEqual(preferences.effectiveDownloadLocation, testDirectory) + XCTAssertEqual(preferences.effectiveDownloadLocation, DownloadsPreferences.defaultDownloadLocation()) } func testWhenGettingSelectedDownloadLocationAndSelectedLocationIsInaccessibleThenDefaultDownloadLocationIsReturned() { @@ -213,18 +213,15 @@ class DownloadsPreferencesTests: XCTestCase { XCTAssertNil(preferences.lastUsedCustomDownloadLocation) } - func testWhenInvalidLastUsedCustomDownloadLocationIsSet_oldValueIsPreserved() { + func testWhenInvalidLastUsedCustomDownloadLocationIsSet_lastUsedCustomLocationIsNil() { let testDirectory = createTemporaryTestDirectory() let persistor = DownloadsPreferencesPersistorMock(selectedDownloadLocation: nil) let preferences = DownloadsPreferences(persistor: persistor) - let valuesBeforeChange = persistor.values() preferences.lastUsedCustomDownloadLocation = testDirectory preferences.lastUsedCustomDownloadLocation = testDirectory.appendingPathComponent("non-existent-dir") - let valuesAfterChange = persistor.values() - XCTAssertEqual(valuesBeforeChange.difference(from: valuesAfterChange), ["\(\DownloadsPreferencesPersistorMock.lastUsedCustomDownloadLocation)".pathExtension]) - XCTAssertEqual(preferences.lastUsedCustomDownloadLocation, testDirectory) + XCTAssertNil(preferences.lastUsedCustomDownloadLocation) } func testWhenLastUsedCustomDownloadLocationIsReset_nilIsReturned() { diff --git a/sandbox-test-tool/SandboxTestTool.swift b/sandbox-test-tool/SandboxTestTool.swift index 2a800cfa18..1234426615 100644 --- a/sandbox-test-tool/SandboxTestTool.swift +++ b/sandbox-test-tool/SandboxTestTool.swift @@ -55,7 +55,11 @@ extension FileLogger: FilePresenterLogger { final class FileLogger { static let shared = FileLogger() - private init() {} + private init() { + if !FileManager.default.fileExists(atPath: fileURL.path) { + FileManager.default.createFile(atPath: fileURL.path, contents: nil) + } + } private let pid = ProcessInfo().processIdentifier @@ -176,7 +180,7 @@ final class SandboxTestToolAppDelegate: NSObject, NSApplicationDelegate { return } do { - let filePresenter = try SandboxFilePresenter(fileBookmarkData: bookmark, logger: logger) + let filePresenter = try BookmarkFilePresenter(fileBookmarkData: bookmark, logger: logger) guard let url = filePresenter.url else { throw NSError(domain: "SandboxTestTool", code: -1, userInfo: [NSLocalizedDescriptionKey: "FilePresenter URL is nil"]) } filePresenter.urlPublisher.dropFirst().sink { [unowned self] url in @@ -188,7 +192,7 @@ final class SandboxTestToolAppDelegate: NSObject, NSApplicationDelegate { self.filePresenters[url] = filePresenter logger.log("📗 openBookmarkWithFilePresenter done: \"\(filePresenter.url?.path ?? "")\"") } catch { - post(.error, with: error.encoded("could not open SandboxFilePresenter")) + post(.error, with: error.encoded("could not open BookmarkFilePresenter")) } } @@ -198,7 +202,6 @@ final class SandboxTestToolAppDelegate: NSObject, NSApplicationDelegate { return } logger.log("🌂 closeFilePresenter for \(path)") - let url = URL(fileURLWithPath: path) filePresenterCancellables[url] = nil filePresenters[url] = nil @@ -230,3 +233,31 @@ private extension Error { return String(data: json!, encoding: .utf8)! } } + +extension NSURL { + + private static var stopAccessingSecurityScopedResourceCallback: ((URL) -> Void)? + + private static let originalStopAccessingSecurityScopedResource = { + class_getInstanceMethod(NSURL.self, #selector(NSURL.stopAccessingSecurityScopedResource))! + }() + private static let swizzledStopAccessingSecurityScopedResource = { + class_getInstanceMethod(NSURL.self, #selector(NSURL.test_tool_stopAccessingSecurityScopedResource))! + }() + private static let swizzleStopAccessingSecurityScopedResourceOnce: Void = { + method_exchangeImplementations(originalStopAccessingSecurityScopedResource, swizzledStopAccessingSecurityScopedResource) + }() + + static func swizzleStopAccessingSecurityScopedResource(with stopAccessingSecurityScopedResourceCallback: ((URL) -> Void)?) { + _=swizzleStopAccessingSecurityScopedResourceOnce + self.stopAccessingSecurityScopedResourceCallback = stopAccessingSecurityScopedResourceCallback + } + + @objc private dynamic func test_tool_stopAccessingSecurityScopedResource() { + if let stopAccessingSecurityScopedResourceCallback = Self.stopAccessingSecurityScopedResourceCallback { + stopAccessingSecurityScopedResourceCallback(self as URL) + } + self.test_tool_stopAccessingSecurityScopedResource() // call original + } + +} From 404b0b04e9b3edf89337e968255dfb0ca7bb267d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 10 Apr 2024 15:04:53 +0500 Subject: [PATCH 057/221] Add supported document types (#2581) Task/Issue URL: https://app.asana.com/0/1177771139624306/1201791966665400/f --- .../Common/Extensions/BundleExtension.swift | 10 + .../Common/Extensions/NSAlertExtension.swift | 9 - DuckDuckGo/Common/Localizables/UserText.swift | 2 - .../SecurityScopedFileURLController.swift | 3 + DuckDuckGo/Info.plist | 476 ++++++++++++++++-- .../View/AddressBarTextField.swift | 8 +- DuckDuckGo/Tab/Model/Tab.swift | 15 +- .../TabExtensions/DownloadsTabExtension.swift | 11 +- 8 files changed, 478 insertions(+), 56 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/BundleExtension.swift b/DuckDuckGo/Common/Extensions/BundleExtension.swift index b3c7a629f5..28133debd8 100644 --- a/DuckDuckGo/Common/Extensions/BundleExtension.swift +++ b/DuckDuckGo/Common/Extensions/BundleExtension.swift @@ -26,6 +26,8 @@ extension Bundle { static let buildNumber = kCFBundleVersionKey as String static let versionNumber = "CFBundleShortVersionString" static let displayName = "CFBundleDisplayName" + static let documentTypes = "CFBundleDocumentTypes" + static let typeExtensions = "CFBundleTypeExtensions" static let vpnMenuAgentBundleId = "AGENT_BUNDLE_ID" static let vpnMenuAgentProductName = "AGENT_PRODUCT_NAME" @@ -115,6 +117,14 @@ extension Bundle { return path.hasPrefix(applicationsPath) } + var documentTypes: [[String: Any]] { + infoDictionary?[Keys.documentTypes] as? [[String: Any]] ?? [] + } + + var fileTypeExtensions: Set { + documentTypes.reduce(into: []) { $0.formUnion($1[Keys.typeExtensions] as? [String] ?? []) } + } + } enum BundleGroup { diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 554ce896e5..a1f71cdeb4 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -137,15 +137,6 @@ extension NSAlert { return alert } - static func noAccessToSelectedFolder() -> NSAlert { - let alert = NSAlert() - alert.messageText = UserText.noAccessToSelectedFolderHeader - alert.informativeText = UserText.noAccessToSelectedFolder - alert.alertStyle = .warning - alert.addButton(withTitle: UserText.cancel) - return alert - } - static func disableEmailProtection() -> NSAlert { let alert = NSAlert() alert.messageText = UserText.disableEmailProtectionTitle diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 4033037606..00a327d353 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -976,8 +976,6 @@ struct UserText { } } - static let noAccessToSelectedFolderHeader = NSLocalizedString("no.access.to.selected.folder.header", value: "DuckDuckGo needs permission to access selected folder", comment: "Header of the alert dialog informing user about failed download") - static let noAccessToSelectedFolder = NSLocalizedString("no.access.to.selected.folder", value: "Grant access to the location of download.", comment: "Alert presented to user if the app doesn't have rights to access selected folder") static let cannotOpenFileAlertHeader = NSLocalizedString("cannot.open.file.alert.header", value: "Cannot Open File", comment: "Header of the alert dialog informing user it is not possible to open the file") static let cannotOpenFileAlertInformative = NSLocalizedString("cannot.open.file.alert.informative", value: "The App Store version of DuckDuckGo can only access local files if you drag-and-drop them into a browser window.\n\n To navigate local files using the address bar, please download DuckDuckGo directly from https://duckduckgo.com/mac.", comment: "Informative of the alert dialog informing user it is not possible to open the file") diff --git a/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift b/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift index 7cdc7eecdf..bec94e02ac 100644 --- a/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift +++ b/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift @@ -152,6 +152,9 @@ extension NSURL { func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(url)) } + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.url === rhs.url + } } static var activeSecurityScopedUrlUsages: Set = [] #endif diff --git a/DuckDuckGo/Info.plist b/DuckDuckGo/Info.plist index 45285233da..095900fb99 100644 --- a/DuckDuckGo/Info.plist +++ b/DuckDuckGo/Info.plist @@ -9,44 +9,444 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDocumentTypes - - - CFBundleTypeExtensions - - html - htm - shtml - xht - xhtml - - CFBundleTypeIconFile - document.icns - CFBundleTypeName - HTML Document - CFBundleTypeOSTypes - - HTML - - CFBundleTypeRole - Viewer - LSHandlerRank - Default - - - CFBundleTypeExtensions - - duckload - - CFBundleTypeName - Incomplete download - CFBundleTypeRole - Editor - NSIsRelatedItemType - - LSHandlerRank - Owner - - + + + CFBundleTypeExtensions + + html + htm + shtml + xht + xhtml + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + HTML Document + CFBundleTypeOSTypes + + HTML + + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + txt + text + log + + CFBundleTypeMIMETypes + + text/plain + + LSItemContentTypes + + public.text + + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + Text document + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + webarchive + + CFBundleTypeMIMETypes + + application/x-webarchive + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + Web archive + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + duckload + + CFBundleTypeName + Incomplete download + CFBundleTypeRole + Editor + NSIsRelatedItemType + + LSHandlerRank + Owner + + + CFBundleTypeExtensions + + pdf + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + PDF Document + CFBundleTypeMIMETypes + + application/pdf + + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + png + + CFBundleTypeMIMETypes + + image/png + + LSItemContentTypes + + public.png + + CFBundleTypeOSTypes + + PNGf + + CFBundleTypeIconFile + document.icns + CFBundleTypeName + PNG image + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + jpg + jpeg + jp2 + jpeg2 + + CFBundleTypeMIMETypes + + image/jpeg + image/jp2 + image/jpeg2000 + + CFBundleTypeOSTypes + + JPEG + jp2 + + LSItemContentTypes + + public.jpeg + public.jpeg-2000 + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + JPEG image + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + gif + giff + + CFBundleTypeMIMETypes + + image/gif + + CFBundleTypeOSTypes + + GIFf + + LSItemContentTypes + + com.compuserve.gif + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + GIF image + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + svg + + CFBundleTypeMIMETypes + + image/svg+xml + + LSItemContentTypes + + public.svg-image + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + SVG image + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + tiff + tif + + CFBundleTypeMIMETypes + + image/tiff + + CFBundleTypeOSTypes + + TIFF + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + TIFF image + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + ico + + CFBundleTypeMIMETypes + + image/x-icon + image/icon + image/ico + + CFBundleTypeOSTypes + + ICO + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + Windows icon + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + bmp + + CFBundleTypeMIMETypes + + image/bmp + image/x-bitmap + image/x-bmp + image/x-ms-bitmap + image/x-ms-bmp + + LSItemContentTypes + + com.microsoft.bmp + + CFBundleTypeName + BMP Image + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + LSRoleHandlerScheme + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + xml + rss + atom + + CFBundleTypeMIMETypes + + text/xml + application/xml + application/rss+xml + application/atom+xml + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + XML document + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + json + + CFBundleTypeMIMETypes + + text/json + application/json + + LSItemContentTypes + + public.json + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + JSON document + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + flac + m4a + mp3 + mpg + wav + aac + amr + mp4 + mpeg + + CFBundleTypeMIMETypes + + audio/flac + audio/x-flac + audio/m4a + audio/mp4 + audio/mp3 + audio/mpeg + audio/mpeg3 + audio/qcp + audio/qcelp + audio/vnd.qcelp + audio/vnd.qcp + audio/vnd.wave + audio/x-wav + audio/x-aac + audio/aac + audio/x-amr + audio/amr + audio/x-m4a + audio/x-mp3 + audio/x-mp4 + audio/x-mpeg + audio/x-mpeg3 + audio/x-mpg + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + Audio File + LSRoleHandlerScheme + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + 3gp + avi + m4v + mov + mp4 + + CFBundleTypeMIMETypes + + video/3gp + video/3gpp + video/avi + video/x-msvideo + video/x-m4v + video/mp4 + video/x-quicktime + video/quicktime + + LSItemContentTypes + + public.movie + + CFBundleTypeIconFile + document.icns + CFBundleTypeIconSystemGenerated + YES + CFBundleTypeName + Video File + LSRoleHandlerScheme + Viewer + LSHandlerRank + Alternate + + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 84fbb30576..b68ce5a6ee 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -332,9 +332,10 @@ final class AddressBarTextField: NSTextField { } #if APPSTORE - if providedUrl.isFileURL, let window = self.window { - let alert = NSAlert.cannotOpenFileAlert() - alert.beginSheetModal(for: window) { response in + if providedUrl.isFileURL, !providedUrl.isWritableLocation(), // is sandbox extension available for the file? + let window = self.window { + + NSAlert.cannotOpenFileAlert().beginSheetModal(for: window) { response in switch response { case .alertSecondButtonReturn: WindowControllersManager.shared.show(url: URL.ddgLearnMore, source: .ui, newTab: false) @@ -344,6 +345,7 @@ final class AddressBarTextField: NSTextField { return } } + return } #endif diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index bfa43c24e3..bac1e48b47 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -1063,8 +1063,11 @@ protocol NewWindowPolicyDecisionMaker { let source = content.source if url.isFileURL { + // WebKit won‘t load local page‘s external resouces even with `allowingReadAccessTo` provided + // this could be fixed using a custom scheme handler loading local resources in future. + let readAccessScopeURL = url return webView.navigator(distributedNavigationDelegate: navigationDelegate) - .loadFileURL(url, allowingReadAccessTo: URL(fileURLWithPath: "/"), withExpectedNavigationType: source.navigationType) + .loadFileURL(url, allowingReadAccessTo: readAccessScopeURL, withExpectedNavigationType: source.navigationType) } var request = URLRequest(url: url, cachePolicy: source.cachePolicy) @@ -1122,7 +1125,12 @@ protocol NewWindowPolicyDecisionMaker { // only restore session from interactionStateData passed to Tab.init guard case .loadCachedFromTabContent(let interactionStateData) = self.interactionState else { return false } - if let url = content.urlForWebView, url.isFileURL { + switch content.urlForWebView { + case .some(let url) where url.isFileURL: +#if APPSTORE + guard url.isWritableLocation() else { fallthrough } +#endif + // request file system access before restoration webView.navigator(distributedNavigationDelegate: navigationDelegate) .loadFileURL(url, allowingReadAccessTo: url)? @@ -1131,7 +1139,8 @@ protocol NewWindowPolicyDecisionMaker { }, navigationDidFail: { [weak self] _, _ in self?.restoreInteractionState(with: interactionStateData) }) - } else { + + default: restoreInteractionState(with: interactionStateData) } diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 2da642e537..23ce6e328e 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -182,7 +182,7 @@ extension DownloadsTabExtension: NavigationResponder { ?? navigationResponse.mainFrameNavigation?.navigationAction guard navigationResponse.httpResponse?.isSuccessful != false, // download non-http responses - !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload + !responseCanShowMIMEType(navigationResponse) || navigationResponse.shouldDownload // if user pressed Opt+Enter in the Address bar to download from a URL || (navigationResponse.mainFrameNavigation?.redirectHistory.last ?? navigationResponse.mainFrameNavigation?.navigationAction)?.navigationType == .custom(.userRequestedPageDownload) else { @@ -199,6 +199,15 @@ extension DownloadsTabExtension: NavigationResponder { return .download } + private func responseCanShowMIMEType(_ response: NavigationResponse) -> Bool { + if response.canShowMIMEType { + return true + } else if response.url.isFileURL { + return Bundle.main.fileTypeExtensions.contains(response.url.pathExtension) + } + return false + } + @MainActor func navigationAction(_ navigationAction: NavigationAction, didBecome download: WebKitDownload) { enqueueDownload(download, withNavigationAction: navigationAction) From 44f0cce6035bf50a22fdb841fb86add94bd4dbf1 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 10 Apr 2024 15:44:22 +0500 Subject: [PATCH 058/221] Disable directory download (#2585) Task/Issue URL: https://app.asana.com/0/1199230911884351/1201043708349615/f --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Common/Extensions/URLExtension.swift | 7 ++ .../TabExtensions/DownloadsTabExtension.swift | 1 + .../Downloads/DownloadsIntegrationTests.swift | 94 +++++++++++++++++++ IntegrationTests/Tab/TabContentTests.swift | 4 +- 5 files changed, 105 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 12fabf86b4..a575d944d1 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", + "location" : "https://github.com/apple/swift-syntax.git", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index b79720a9d5..3d95ae07f1 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -475,6 +475,13 @@ extension URL { } } + var isDirectory: Bool { + var isDirectory: ObjCBool = false + guard isFileURL, + FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { return false } + return isDirectory.boolValue + } + mutating func setFileHidden(_ hidden: Bool) throws { var resourceValues = URLResourceValues() resourceValues.isHidden = true diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift index 23ce6e328e..1aae34f4c6 100644 --- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift @@ -182,6 +182,7 @@ extension DownloadsTabExtension: NavigationResponder { ?? navigationResponse.mainFrameNavigation?.navigationAction guard navigationResponse.httpResponse?.isSuccessful != false, // download non-http responses + !navigationResponse.url.isDirectory, // don‘t download a local directory !responseCanShowMIMEType(navigationResponse) || navigationResponse.shouldDownload // if user pressed Opt+Enter in the Address bar to download from a URL || (navigationResponse.mainFrameNavigation?.redirectHistory.last ?? navigationResponse.mainFrameNavigation?.navigationAction)?.navigationType == .custom(.userRequestedPageDownload) diff --git a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift index d108921c11..fbe3f5f557 100644 --- a/IntegrationTests/Downloads/DownloadsIntegrationTests.swift +++ b/IntegrationTests/Downloads/DownloadsIntegrationTests.swift @@ -91,6 +91,100 @@ class DownloadsIntegrationTests: XCTestCase { XCTAssertEqual(try? Data(contentsOf: fileUrl), data.html) } + @MainActor + func testWhenUnsupportedMimeType_downloadStarts() async throws { + let preferences = DownloadsPreferences.shared + preferences.alwaysRequestDownloadLocation = false + preferences.selectedDownloadLocation = FileManager.default.temporaryDirectory + + let downloadTaskFuture = FileDownloadManager.shared.downloadsPublisher.timeout(5).first().promise() + let suffix = Int.random(in: 0.. Date: Wed, 10 Apr 2024 20:01:44 +0500 Subject: [PATCH 059/221] drop Downloads storyboard (#2556) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206643583260394/f --- DuckDuckGo.xcodeproj/project.pbxproj | 70 ++- .../View/BookmarkOutlineCellView.swift | 2 + DuckDuckGo/Common/Localizables/UserText.swift | 2 +- .../View/AppKit/PreviewViewController.swift | 77 +++ .../Model}/DownloadListStoreMock.swift | 5 +- .../FileDownload/View/Downloads.storyboard | 483 ------------------ .../FileDownload/View/DownloadsCellView.swift | 231 ++++++++- .../FileDownload/View/DownloadsPopover.swift | 2 +- .../View/DownloadsViewController.swift | 266 ++++++---- .../View/NoDownloadsCellView.swift | 82 +++ .../View/OpenDownloadsCellView.swift | 67 +++ DuckDuckGo/Localizable.xcstrings | 60 --- 12 files changed, 672 insertions(+), 675 deletions(-) create mode 100644 DuckDuckGo/Common/View/AppKit/PreviewViewController.swift rename {UnitTests/FileDownload/Helpers => DuckDuckGo/FileDownload/Model}/DownloadListStoreMock.swift (92%) delete mode 100644 DuckDuckGo/FileDownload/View/Downloads.storyboard create mode 100644 DuckDuckGo/FileDownload/View/NoDownloadsCellView.swift create mode 100644 DuckDuckGo/FileDownload/View/OpenDownloadsCellView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0ece6426d8..415aeb2e28 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -706,7 +706,6 @@ 3706FCB8293F65D500E42796 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85B7184927677C2D00B4277F /* Onboarding.storyboard */; }; 3706FCB9293F65D500E42796 /* FireproofDomains.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B0511AD262CAA5A00F6079C /* FireproofDomains.storyboard */; }; 3706FCBA293F65D500E42796 /* clickToLoadConfig.json in Resources */ = {isa = PBXBuildFile; fileRef = EA47767F272A21B700419EDA /* clickToLoadConfig.json */; }; - 3706FCBB293F65D500E42796 /* Downloads.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6B1E88126D5DAC30062C350 /* Downloads.storyboard */; }; 3706FCBC293F65D500E42796 /* dark-shield.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396F2754D4E900B241FA /* dark-shield.json */; }; 3706FCBD293F65D500E42796 /* dark-shield-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6EA27E880AE00036718 /* dark-shield-mouse-over.json */; }; 3706FCBE293F65D500E42796 /* autoconsent-bundle.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055C327A1BA1D001AC618 /* autoconsent-bundle.js */; }; @@ -805,7 +804,6 @@ 3706FE10293F661700E42796 /* TestNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E02913AEDB00225DE2 /* TestNavigationDelegate.swift */; }; 3706FE11293F661700E42796 /* URLSuggestedFilenameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8553FF51257523760029327F /* URLSuggestedFilenameTests.swift */; }; 3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC3B4825DAC9BD00C7D2AA /* ConfigurationStorageTests.swift */; }; - 3706FE14293F661700E42796 /* DownloadListStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */; }; 3706FE15293F661700E42796 /* PrivacyIconViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA91F83827076F1900771A0D /* PrivacyIconViewModelTests.swift */; }; 3706FE16293F661700E42796 /* CSVImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B723E0126B0003E00E14D75 /* CSVImporterTests.swift */; }; 3706FE19293F661700E42796 /* DeviceAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC16A427C488C900E00A38 /* DeviceAuthenticatorTests.swift */; }; @@ -1971,7 +1969,6 @@ 4B957BF32AC7AE700062CA31 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85B7184927677C2D00B4277F /* Onboarding.storyboard */; }; 4B957BF42AC7AE700062CA31 /* FireproofDomains.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B0511AD262CAA5A00F6079C /* FireproofDomains.storyboard */; }; 4B957BF52AC7AE700062CA31 /* clickToLoadConfig.json in Resources */ = {isa = PBXBuildFile; fileRef = EA47767F272A21B700419EDA /* clickToLoadConfig.json */; }; - 4B957BF62AC7AE700062CA31 /* Downloads.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6B1E88126D5DAC30062C350 /* Downloads.storyboard */; }; 4B957BF72AC7AE700062CA31 /* dark-shield.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396F2754D4E900B241FA /* dark-shield.json */; }; 4B957BF82AC7AE700062CA31 /* BookmarksBarPromptAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 859F30662A72B38500C20372 /* BookmarksBarPromptAssets.xcassets */; }; 4B957BF92AC7AE700062CA31 /* dark-shield-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6EA27E880AE00036718 /* dark-shield-mouse-over.json */; }; @@ -2931,7 +2928,6 @@ B662D3DF275616FF0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B662D3DD275613BB0035D4D6 /* EncryptionKeyStoreMock.swift */; }; B6656E0D2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6656E0C2B29C733008798A1 /* FileImportViewLocalizationTests.swift */; }; B6656E0E2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6656E0C2B29C733008798A1 /* FileImportViewLocalizationTests.swift */; }; - B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */; }; B6656E5B2B2ADB1C008798A1 /* RequestFilePermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B5F5832B03580A008DB58A /* RequestFilePermissionView.swift */; }; B6676BE12AA986A700525A21 /* AddressBarTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6676BE02AA986A700525A21 /* AddressBarTextEditor.swift */; }; B6676BE22AA986A700525A21 /* AddressBarTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6676BE02AA986A700525A21 /* AddressBarTextEditor.swift */; }; @@ -3080,7 +3076,6 @@ B6B1E87B26D381710062C350 /* DownloadListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E87A26D381710062C350 /* DownloadListCoordinator.swift */; }; B6B1E87E26D5DA0E0062C350 /* DownloadsPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E87D26D5DA0E0062C350 /* DownloadsPopover.swift */; }; B6B1E88026D5DA9B0062C350 /* DownloadsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E87F26D5DA9B0062C350 /* DownloadsViewController.swift */; }; - B6B1E88226D5DAC30062C350 /* Downloads.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6B1E88126D5DAC30062C350 /* Downloads.storyboard */; }; B6B1E88426D5EB570062C350 /* DownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E88326D5EB570062C350 /* DownloadsCellView.swift */; }; B6B1E88B26D774090062C350 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E88A26D774090062C350 /* LinkButton.swift */; }; B6B2400E28083B49001B8F3A /* WebViewContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B2400D28083B49001B8F3A /* WebViewContainerView.swift */; }; @@ -3185,6 +3180,18 @@ B6E1491029A5C30500AAFBE8 /* ContentBlockingTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D574B12947224C008ED1B6 /* ContentBlockingTabExtension.swift */; }; B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D574B329472253008ED1B6 /* FBProtectionTabExtension.swift */; }; B6E319382953446000DD3BCF /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E319372953446000DD3BCF /* Assertions.swift */; }; + B6E3E5502BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E54F2BBFCDEE00A41922 /* OpenDownloadsCellView.swift */; }; + B6E3E5512BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E54F2BBFCDEE00A41922 /* OpenDownloadsCellView.swift */; }; + B6E3E5522BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E54F2BBFCDEE00A41922 /* OpenDownloadsCellView.swift */; }; + B6E3E5542BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E5532BBFCEE300A41922 /* NoDownloadsCellView.swift */; }; + B6E3E5552BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E5532BBFCEE300A41922 /* NoDownloadsCellView.swift */; }; + B6E3E5562BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E5532BBFCEE300A41922 /* NoDownloadsCellView.swift */; }; + B6E3E5582BBFD51400A41922 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E5572BBFD51400A41922 /* PreviewViewController.swift */; }; + B6E3E5592BBFD51400A41922 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E5572BBFD51400A41922 /* PreviewViewController.swift */; }; + B6E3E55A2BBFD51400A41922 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E3E5572BBFD51400A41922 /* PreviewViewController.swift */; }; + B6E3E55B2BC0041900A41922 /* DownloadListStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */; }; + B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */; }; + B6E3E55D2BC0041C00A41922 /* DownloadListStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */; }; B6E61EE3263AC0C8004E11AB /* FileManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */; }; B6E6B9E32BA1F5F1008AA7E1 /* FilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E6B9E22BA1F5F1008AA7E1 /* FilePresenter.swift */; }; B6E6B9E42BA1F5F1008AA7E1 /* FilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E6B9E22BA1F5F1008AA7E1 /* FilePresenter.swift */; }; @@ -4663,7 +4670,6 @@ B6B1E87A26D381710062C350 /* DownloadListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListCoordinator.swift; sourceTree = ""; }; B6B1E87D26D5DA0E0062C350 /* DownloadsPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsPopover.swift; sourceTree = ""; }; B6B1E87F26D5DA9B0062C350 /* DownloadsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewController.swift; sourceTree = ""; }; - B6B1E88126D5DAC30062C350 /* Downloads.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Downloads.storyboard; sourceTree = ""; }; B6B1E88326D5EB570062C350 /* DownloadsCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsCellView.swift; sourceTree = ""; }; B6B1E88A26D774090062C350 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; B6B2400D28083B49001B8F3A /* WebViewContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewContainerView.swift; sourceTree = ""; }; @@ -4727,6 +4733,9 @@ B6DB3CFA26A17CB800D459B7 /* PermissionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionModel.swift; sourceTree = ""; }; B6DE57F52B05EA9000CD54B9 /* SheetHostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetHostingWindow.swift; sourceTree = ""; }; B6E319372953446000DD3BCF /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = ""; }; + B6E3E54F2BBFCDEE00A41922 /* OpenDownloadsCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenDownloadsCellView.swift; sourceTree = ""; }; + B6E3E5532BBFCEE300A41922 /* NoDownloadsCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoDownloadsCellView.swift; sourceTree = ""; }; + B6E3E5572BBFD51400A41922 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExtension.swift; sourceTree = ""; }; B6E6B9E22BA1F5F1008AA7E1 /* FilePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePresenter.swift; sourceTree = ""; }; B6E6B9E82BA1FA1C008AA7E1 /* SandboxTestTool.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SandboxTestTool.xcconfig; sourceTree = ""; }; @@ -6019,8 +6028,8 @@ 4B67743D255DBEEA00025BD8 /* Database */ = { isa = PBXGroup; children = ( - 4B677440255DBEEA00025BD8 /* Database.swift */, B6085D052743905F00A9C456 /* CoreDataStore.swift */, + 4B677440255DBEEA00025BD8 /* Database.swift */, ); path = Database; sourceTree = ""; @@ -6694,6 +6703,7 @@ isa = PBXGroup; children = ( B6C0B23526E732000031CB7F /* DownloadListItem.swift */, + B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */, B6C0B23D26E8BF1F0031CB7F /* DownloadListViewModel.swift */, B6CC26672BAD959500F53F8D /* DownloadProgress.swift */, B6104E9A2BA9C173008636B2 /* DownloadResumeData.swift */, @@ -6701,10 +6711,10 @@ B6C0B23826E742610031CB7F /* FileDownloadError.swift */, 856C98DE257014BD00A22F1F /* FileDownloadManager.swift */, B6E6B9E22BA1F5F1008AA7E1 /* FilePresenter.swift */, - B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */, + B6CC266B2BAD9CD800F53F8D /* FileProgressPresenter.swift */, B6ABD0CD2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m */, + B6ABD0C92BC03F610000EB69 /* SecurityScopedFileURLController.swift */, B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */, - B6CC266B2BAD9CD800F53F8D /* FileProgressPresenter.swift */, ); path = Model; sourceTree = ""; @@ -6768,26 +6778,27 @@ 8585B63626D6E61500C1416F /* AppKit */ = { isa = PBXGroup; children = ( + 85774B022A71CDD000DE0561 /* BlockMenuItem.swift */, B65E6B9D26D9EC0800095F96 /* CircularProgressView.swift */, B693954626F04BEA0015B914 /* ColorView.swift */, + 4B379C2327BDE1B0008A968E /* FlatButton.swift */, B693953E26F04BE70015B914 /* FocusRingView.swift */, B693954326F04BE90015B914 /* GradientView.swift */, - B6B1E88A26D774090062C350 /* LinkButton.swift */, B6B140872ABDBCC1004F8E85 /* HoverTrackingArea.swift */, + B6B1E88A26D774090062C350 /* LinkButton.swift */, + B693954026F04BE80015B914 /* LoadingProgressView.swift */, B693954426F04BE90015B914 /* LongPressButton.swift */, - B693954926F04BEB0015B914 /* MouseOverButton.swift */, AA7EB6DE27E7C57D00036718 /* MouseOverAnimationButton.swift */, + B693954926F04BEB0015B914 /* MouseOverButton.swift */, B693953D26F04BE70015B914 /* MouseOverView.swift */, - 4B379C2327BDE1B0008A968E /* FlatButton.swift */, + 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */, B693954726F04BEA0015B914 /* NSSavePanelExtension.swift */, B693954126F04BE80015B914 /* PaddedImageButton.swift */, - B693954026F04BE80015B914 /* LoadingProgressView.swift */, + B6E3E5572BBFD51400A41922 /* PreviewViewController.swift */, B60C6F8C29B200AB007BFAA8 /* SavePanelAccessoryView.swift */, B693954226F04BE90015B914 /* ShadowView.swift */, - B693954526F04BEA0015B914 /* WindowDraggingView.swift */, 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */, - 85774B022A71CDD000DE0561 /* BlockMenuItem.swift */, - 1D26EBAB2B74BECB0002A93F /* NSImageSendable.swift */, + B693954526F04BEA0015B914 /* WindowDraggingView.swift */, ); path = AppKit; sourceTree = ""; @@ -8589,11 +8600,12 @@ B6B1E87C26D5DA020062C350 /* View */ = { isa = PBXGroup; children = ( + B6B1E88326D5EB570062C350 /* DownloadsCellView.swift */, B6B1E87D26D5DA0E0062C350 /* DownloadsPopover.swift */, B6B1E87F26D5DA9B0062C350 /* DownloadsViewController.swift */, - B6B1E88126D5DAC30062C350 /* Downloads.storyboard */, - B6B1E88326D5EB570062C350 /* DownloadsCellView.swift */, + B6E3E5532BBFCEE300A41922 /* NoDownloadsCellView.swift */, B6C0B23B26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift */, + B6E3E54F2BBFCDEE00A41922 /* OpenDownloadsCellView.swift */, ); path = View; sourceTree = ""; @@ -8638,9 +8650,9 @@ B6C0B23126E71A800031CB7F /* Services */ = { isa = PBXGroup; children = ( - B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */, - B6C0B22F26E61D630031CB7F /* DownloadListStore.swift */, B6B1E87A26D381710062C350 /* DownloadListCoordinator.swift */, + B6C0B22F26E61D630031CB7F /* DownloadListStore.swift */, + B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */, ); path = Services; sourceTree = ""; @@ -8698,7 +8710,6 @@ B693956726F352DB0015B914 /* DownloadsWebViewMock.h */, B693956826F352DB0015B914 /* DownloadsWebViewMock.m */, B630794126731F5400DCEE41 /* WKDownloadMock.swift */, - B693956026F1C1BC0015B914 /* DownloadListStoreMock.swift */, B693956226F1C2A40015B914 /* FileDownloadManagerMock.swift */, 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */, ); @@ -9596,7 +9607,6 @@ 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */, 3706FCB9293F65D500E42796 /* FireproofDomains.storyboard in Resources */, 3706FCBA293F65D500E42796 /* clickToLoadConfig.json in Resources */, - 3706FCBB293F65D500E42796 /* Downloads.storyboard in Resources */, 3706FCBC293F65D500E42796 /* dark-shield.json in Resources */, 854DAAAE2A72B613001E2E24 /* BookmarksBarPromptAssets.xcassets in Resources */, 3706FCBD293F65D500E42796 /* dark-shield-mouse-over.json in Resources */, @@ -9725,7 +9735,6 @@ 56CEE9102B7A72FE00CF10AA /* InfoPlist.xcstrings in Resources */, 4B957BF42AC7AE700062CA31 /* FireproofDomains.storyboard in Resources */, 4B957BF52AC7AE700062CA31 /* clickToLoadConfig.json in Resources */, - 4B957BF62AC7AE700062CA31 /* Downloads.storyboard in Resources */, 4B957BF72AC7AE700062CA31 /* dark-shield.json in Resources */, 4B957BF82AC7AE700062CA31 /* BookmarksBarPromptAssets.xcassets in Resources */, 4B957BF92AC7AE700062CA31 /* dark-shield-mouse-over.json in Resources */, @@ -9841,7 +9850,6 @@ 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */, 4B0511C3262CAA5A00F6079C /* FireproofDomains.storyboard in Resources */, EA477680272A21B700419EDA /* clickToLoadConfig.json in Resources */, - B6B1E88226D5DAC30062C350 /* Downloads.storyboard in Resources */, AA3439712754D4E900B241FA /* dark-shield.json in Resources */, 859F30672A72B38500C20372 /* BookmarksBarPromptAssets.xcassets in Resources */, AA7EB6EB27E880AE00036718 /* dark-shield-mouse-over.json in Resources */, @@ -10340,6 +10348,7 @@ 3706FAAD293F65D500E42796 /* BadgeNotificationAnimationModel.swift in Sources */, 3706FAAE293F65D500E42796 /* HyperLink.swift in Sources */, 3706FAAF293F65D500E42796 /* PasteboardWriting.swift in Sources */, + B6E3E5512BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */, 3706FAB0293F65D500E42796 /* BookmarkOutlineCellView.swift in Sources */, 3706FAB1293F65D500E42796 /* UnprotectedDomains.xcdatamodeld in Sources */, 85393C872A6FF1B600F11EB3 /* BookmarksBarAppearance.swift in Sources */, @@ -10479,6 +10488,7 @@ 3706FB17293F65D500E42796 /* FirePopoverCollectionViewHeader.swift in Sources */, 85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */, 3706FB19293F65D500E42796 /* FireViewController.swift in Sources */, + B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */, 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */, 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, @@ -10843,6 +10853,7 @@ 3706FC13293F65D500E42796 /* FaviconView.swift in Sources */, B69A14F72B4D701F00B9417D /* AddBookmarkPopoverViewModel.swift in Sources */, 3706FC14293F65D500E42796 /* OnboardingFlow.swift in Sources */, + B6E3E5592BBFD51400A41922 /* PreviewViewController.swift in Sources */, EEC8EB3E2982CA3B0065AA39 /* JSAlertViewModel.swift in Sources */, 3706FC16293F65D500E42796 /* PasswordManagementLoginModel.swift in Sources */, 3706FC17293F65D500E42796 /* TabViewModel.swift in Sources */, @@ -10923,6 +10934,7 @@ 3706FC4E293F65D500E42796 /* AtbAndVariantCleanup.swift in Sources */, 3706FC50293F65D500E42796 /* FeedbackWindow.swift in Sources */, 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, + B6E3E5552BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, @@ -11120,7 +11132,6 @@ 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, 3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */, - 3706FE14293F661700E42796 /* DownloadListStoreMock.swift in Sources */, 3706FE15293F661700E42796 /* PrivacyIconViewModelTests.swift in Sources */, B68412212B6A30680092F66A /* StringExtensionTests.swift in Sources */, 1D8C2FEE2B70F5D0005E4BBD /* MockViewSnapshotRenderer.swift in Sources */, @@ -11628,6 +11639,7 @@ 1D01A3D22B88CEC600FE8150 /* PreferencesAccessibilityView.swift in Sources */, 4B9579A02AC7AE700062CA31 /* InvitedToWaitlistView.swift in Sources */, 4B9579A22AC7AE700062CA31 /* SaveCredentialsViewController.swift in Sources */, + B6E3E55D2BC0041C00A41922 /* DownloadListStoreMock.swift in Sources */, 4B9579A32AC7AE700062CA31 /* PopUpButton.swift in Sources */, 4B9579A42AC7AE700062CA31 /* NetworkProtectionInviteDialog.swift in Sources */, 4B9579A52AC7AE700062CA31 /* SuggestionViewController.swift in Sources */, @@ -11660,6 +11672,7 @@ 4B9579C02AC7AE700062CA31 /* DebugUserScript.swift in Sources */, 1DC669722B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, 4B9579C12AC7AE700062CA31 /* RecentlyClosedTab.swift in Sources */, + B6E3E55A2BBFD51400A41922 /* PreviewViewController.swift in Sources */, 4B9579C22AC7AE700062CA31 /* PDFSearchTextMenuItemHandler.swift in Sources */, 4B9579C42AC7AE700062CA31 /* HistoryMenu.swift in Sources */, 4B9579C52AC7AE700062CA31 /* ContentScopeFeatureFlagging.swift in Sources */, @@ -11860,6 +11873,7 @@ 4B957A6A2AC7AE700062CA31 /* SecureVaultLoginImporter.swift in Sources */, 4B957A6B2AC7AE700062CA31 /* WKProcessPoolExtension.swift in Sources */, 4B957A6D2AC7AE700062CA31 /* LoginItemsManager.swift in Sources */, + B6E3E5562BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, 4B957A6E2AC7AE700062CA31 /* PixelExperiment.swift in Sources */, 4B957A6F2AC7AE700062CA31 /* DuckPlayerTabExtension.swift in Sources */, 4B957A702AC7AE700062CA31 /* RecentlyClosedCoordinator.swift in Sources */, @@ -12100,6 +12114,7 @@ 7BEC20472B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift in Sources */, 4B957B392AC7AE700062CA31 /* StatisticsStore.swift in Sources */, EEC4A66B2B2C87D300F7C0AA /* VPNLocationView.swift in Sources */, + B6E3E5522BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */, 4B957B3A2AC7AE700062CA31 /* BWInstallationService.swift in Sources */, 4B957B3B2AC7AE700062CA31 /* BookmarksBarPromptPopover.swift in Sources */, 4B957B3C2AC7AE700062CA31 /* NetworkProtectionInvitePresenter.swift in Sources */, @@ -12372,6 +12387,7 @@ 0230C0A3272080090018F728 /* KeyedCodingExtension.swift in Sources */, 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */, B6BF5D852946FFDA006742B1 /* PrivacyDashboardTabExtension.swift in Sources */, + B6E3E55B2BC0041900A41922 /* DownloadListStoreMock.swift in Sources */, B6C0B23026E61D630031CB7F /* DownloadListStore.swift in Sources */, 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */, AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, @@ -12457,6 +12473,7 @@ AA3D531727A1EEED00074EC1 /* FeedbackViewController.swift in Sources */, AAEF6BC8276A081C0024DCF4 /* FaviconSelector.swift in Sources */, 4B2E7D6326FF9D6500D2DB17 /* PrintingUserScript.swift in Sources */, + B6E3E5542BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, 4BBDEE9428FC14760092FAA6 /* ConnectBitwardenViewController.swift in Sources */, 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */, 9833912F27AAA3CE00DAF119 /* AppTrackerDataSetProvider.swift in Sources */, @@ -12944,6 +12961,7 @@ B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */, 8589063A267BCD8E00D23B0D /* SaveCredentialsPopover.swift in Sources */, 987799F32999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, + B6E3E5582BBFD51400A41922 /* PreviewViewController.swift in Sources */, 4B379C1527BD91E3008A968E /* QuartzIdleStateProvider.swift in Sources */, 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, 1D01A3D42B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, @@ -13075,6 +13093,7 @@ 1DDD3EC02B84F5D5004CBF2B /* PreferencesCookiePopupProtectionView.swift in Sources */, B693955026F04BEB0015B914 /* ShadowView.swift in Sources */, AA3D531D27A2F58F00074EC1 /* FeedbackSender.swift in Sources */, + B6E3E5502BBFCDEE00A41922 /* OpenDownloadsCellView.swift in Sources */, B6BDDA012942389000F68088 /* TabExtensions.swift in Sources */, AA7412B224D0B3AC00D22FE0 /* TabBarViewItem.swift in Sources */, 856C98D52570116900A22F1F /* NSWindow+Toast.swift in Sources */, @@ -13159,7 +13178,6 @@ 1D3B1ABF29369FC8006F4388 /* BWEncryptionTests.swift in Sources */, B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, 1D9FDEC62B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, - B6656E122B29E3BE008798A1 /* DownloadListStoreMock.swift in Sources */, 37D23780287EFEE200BCE03B /* PinnedTabsManagerTests.swift in Sources */, AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */, 9FA75A3E2BA00E1400DA5FA6 /* BookmarksBarMenuFactoryTests.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift index 06b868c826..813e1ff8ec 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift @@ -40,6 +40,8 @@ final class BookmarkOutlineCellView: NSTableCellView { init(identifier: NSUserInterfaceItemIdentifier) { super.init(frame: .zero) + self.identifier = identifier + setupUI() } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 00a327d353..36e9c3d90d 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1061,7 +1061,7 @@ struct UserText { static let downloadsOpenWebsiteItem = NSLocalizedString("downloads.open-website.item", value: "Open Originating Website", comment: "Contextual menu item in downloads manager to open the downloaded file originating website") static let downloadsRemoveFromListItem = NSLocalizedString("downloads.remove-from-list.item", value: "Remove from List", comment: "Contextual menu item in downloads manager to remove the given downloaded from the list of downloaded files") static let downloadsStopItem = NSLocalizedString("downloads.stop.item", value: "Stop", comment: "Contextual menu item in downloads manager to stop the download") - static let downloadsRestartItem = NSLocalizedString("downloads.restart.item", value: "Stop", comment: "Contextual menu item in downloads manager to restart the download") + static let downloadsRestartItem = restartDownloadToolTip static let downloadsClearAllItem = NSLocalizedString("downloads.clear-all.item", value: "Clear All", comment: "Contextual menu item in downloads manager to clear all downloaded items from the list") static let downloadsNoRecentDownload = NSLocalizedString("downloads.no-recent-downloads", value: "No recent downloads", comment: "Label in the downloads manager that shows that there are no recently downloaded items") static let downloadsOpenDownloadsFolder = NSLocalizedString("downloads.open-downloads-folder", value: "Open Downloads Folder", comment: "Button in the downloads manager that allows the user to open the downloads folder") diff --git a/DuckDuckGo/Common/View/AppKit/PreviewViewController.swift b/DuckDuckGo/Common/View/AppKit/PreviewViewController.swift new file mode 100644 index 0000000000..d62feef70b --- /dev/null +++ b/DuckDuckGo/Common/View/AppKit/PreviewViewController.swift @@ -0,0 +1,77 @@ +// +// PreviewViewController.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 AppKit + +@resultBuilder +struct NSViewBuilder { + static func buildBlock(_ component: NSView) -> NSView { + return component + } +} + +#if DEBUG +/// Used to preview an NSView using Xcode #Preview macro +/// Usage: +/// ``` +/// @available(macOS 14.0, *) +/// #Preview { +/// PreviewViewController(showWindowTitle: false /*hide preview window title*/, adjustWindowFrame: true /*set the window size to the view size*/) { +/// MyNSView() +/// } +/// } +/// ``` +@available(macOS 14.0, *) +final class PreviewViewController: NSViewController { + let showWindowTitle: Bool + let adjustWindowFrame: Bool + + init(showWindowTitle: Bool = true, adjustWindowFrame: Bool = false, @NSViewBuilder builder: () -> NSView) { + self.showWindowTitle = showWindowTitle + self.adjustWindowFrame = adjustWindowFrame + super.init(nibName: nil, bundle: nil) + self.view = builder() + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + override func viewDidAppear() { + guard let window = view.window else { return } + if !showWindowTitle { + window.titlebarAppearsTransparent = true + window.titleVisibility = .hidden + window.styleMask = [] + } + if adjustWindowFrame { + window.setFrame(NSRect(origin: .zero, size: view.bounds.size), display: true) + } + } + +} +#else +final class PreviewViewController: NSViewController { + init(showWindowTitle: Bool = true, adjustWindowFrame: Bool = false, @NSViewBuilder builder: () -> NSView) { + fatalError("only for DEBUG") + } + required init?(coder: NSCoder) { + fatalError("only for DEBUG") + } +} +#endif diff --git a/UnitTests/FileDownload/Helpers/DownloadListStoreMock.swift b/DuckDuckGo/FileDownload/Model/DownloadListStoreMock.swift similarity index 92% rename from UnitTests/FileDownload/Helpers/DownloadListStoreMock.swift rename to DuckDuckGo/FileDownload/Model/DownloadListStoreMock.swift index 28001ba4e1..edae68b750 100644 --- a/UnitTests/FileDownload/Helpers/DownloadListStoreMock.swift +++ b/DuckDuckGo/FileDownload/Model/DownloadListStoreMock.swift @@ -17,12 +17,12 @@ // import Foundation -@testable import DuckDuckGo_Privacy_Browser +#if DEBUG final class DownloadListStoreMock: DownloadListStoring { var fetchBlock: ((@escaping @MainActor (Result<[DownloadListItem], Error>) -> Void) -> Void)? - func fetch(completionHandler: @escaping @MainActor (Result<[DuckDuckGo_Privacy_Browser.DownloadListItem], any Error>) -> Void) { + func fetch(completionHandler: @escaping @MainActor (Result<[DownloadListItem], any Error>) -> Void) { fetchBlock?(completionHandler) } @@ -42,3 +42,4 @@ final class DownloadListStoreMock: DownloadListStoring { } } +#endif diff --git a/DuckDuckGo/FileDownload/View/Downloads.storyboard b/DuckDuckGo/FileDownload/View/Downloads.storyboard deleted file mode 100644 index 41e5511da3..0000000000 --- a/DuckDuckGo/FileDownload/View/Downloads.storyboard +++ /dev/null @@ -1,483 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/FileDownload/View/DownloadsCellView.swift b/DuckDuckGo/FileDownload/View/DownloadsCellView.swift index ad3ed7bdd5..2c3f803924 100644 --- a/DuckDuckGo/FileDownload/View/DownloadsCellView.swift +++ b/DuckDuckGo/FileDownload/View/DownloadsCellView.swift @@ -22,6 +22,11 @@ import UniformTypeIdentifiers final class DownloadsCellView: NSTableCellView { + fileprivate enum Constants { + static let width: CGFloat = 420 + static let height: CGFloat = 60 + } + enum DownloadError: Error { case urlNotSet case fileRemoved @@ -38,13 +43,22 @@ final class DownloadsCellView: NSTableCellView { } } - @IBOutlet var titleLabel: NSTextField! - @IBOutlet var detailLabel: NSTextField! - @IBOutlet var progressView: CircularProgressView! - @IBOutlet var cancelButton: MouseOverButton! - @IBOutlet var revealButton: MouseOverButton! - @IBOutlet var restartButton: MouseOverButton! - @IBOutlet var separator: NSBox! + private let fileIconView = NSImageView() + private let titleLabel = NSTextField() + private let detailLabel = NSTextField() + + private let progressView = CircularProgressView() + private let cancelButton = MouseOverButton(image: .cancelDownload, + target: nil, + action: #selector(DownloadsViewController.cancelDownloadAction)) + private let revealButton = MouseOverButton(image: .revealDownload, + target: nil, + action: #selector(DownloadsViewController.revealDownloadAction)) + private let restartButton = MouseOverButton(image: .restartDownload, + target: nil, + action: #selector(DownloadsViewController.restartDownloadAction)) + + private let separator = NSBox() private var buttonOverCancellables = Set() private var cancellables = Set() @@ -82,7 +96,149 @@ final class DownloadsCellView: NSTableCellView { } } - override func awakeFromNib() { + init(identifier: NSUserInterfaceItemIdentifier) { + super.init(frame: CGRect(x: 0, y: 0, width: Constants.width, height: Constants.height)) + self.identifier = identifier + + setupUI() + subscribeToMouseOverEvents() + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + // swiftlint:disable:next function_body_length + private func setupUI() { + self.imageView = fileIconView + self.wantsLayer = true + + addSubview(fileIconView) + addSubview(titleLabel) + addSubview(detailLabel) + addSubview(cancelButton) + addSubview(revealButton) + addSubview(restartButton) + addSubview(progressView) + addSubview(separator) + + fileIconView.translatesAutoresizingMaskIntoConstraints = false + fileIconView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + fileIconView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + fileIconView.imageScaling = .scaleProportionallyDown + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.isEditable = false + titleLabel.isBordered = false + titleLabel.isSelectable = false + titleLabel.drawsBackground = false + titleLabel.font = .systemFont(ofSize: 13) + titleLabel.textColor = .controlTextColor + titleLabel.lineBreakMode = .byTruncatingMiddle + titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + titleLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + detailLabel.isEditable = false + detailLabel.isBordered = false + detailLabel.isSelectable = false + detailLabel.drawsBackground = false + detailLabel.font = .systemFont(ofSize: 13) + detailLabel.textColor = .secondaryLabelColor + detailLabel.lineBreakMode = .byClipping + detailLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + detailLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + + progressView.translatesAutoresizingMaskIntoConstraints = false + + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + cancelButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + cancelButton.bezelStyle = .shadowlessSquare + cancelButton.isBordered = false + cancelButton.imagePosition = .imageOnly + cancelButton.imageScaling = .scaleProportionallyDown + cancelButton.cornerRadius = 4 + cancelButton.backgroundInset = CGPoint(x: 2, y: 2) + cancelButton.mouseDownColor = .buttonMouseDown + cancelButton.mouseOverColor = .buttonMouseOver + + revealButton.translatesAutoresizingMaskIntoConstraints = false + revealButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + revealButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + revealButton.alignment = .center + revealButton.bezelStyle = .shadowlessSquare + revealButton.isBordered = false + revealButton.imagePosition = .imageOnly + revealButton.imageScaling = .scaleProportionallyDown + revealButton.cornerRadius = 4 + revealButton.backgroundInset = CGPoint(x: 2, y: 2) + revealButton.mouseDownColor = .buttonMouseDown + revealButton.mouseOverColor = .buttonMouseOver + + restartButton.translatesAutoresizingMaskIntoConstraints = false + restartButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + restartButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + restartButton.alignment = .center + restartButton.bezelStyle = .shadowlessSquare + restartButton.isBordered = false + restartButton.imagePosition = .imageOnly + restartButton.imageScaling = .scaleProportionallyDown + restartButton.cornerRadius = 4 + restartButton.backgroundInset = CGPoint(x: 2, y: 2) + restartButton.mouseDownColor = .buttonMouseDown + restartButton.mouseOverColor = .buttonMouseOver + + separator.boxType = .separator + separator.translatesAutoresizingMaskIntoConstraints = false + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + fileIconView.heightAnchor.constraint(equalToConstant: 32), + fileIconView.widthAnchor.constraint(equalToConstant: 32), + fileIconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 7), + fileIconView.centerYAnchor.constraint(equalTo: centerYAnchor), + + titleLabel.heightAnchor.constraint(equalToConstant: 16), + titleLabel.leadingAnchor.constraint(equalTo: fileIconView.trailingAnchor, constant: 6), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12), + detailLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + + cancelButton.heightAnchor.constraint(equalToConstant: 32), + cancelButton.widthAnchor.constraint(equalToConstant: 32), + cancelButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + cancelButton.leadingAnchor.constraint(equalTo: detailLabel.trailingAnchor, constant: 8), + cancelButton.centerYAnchor.constraint(equalTo: centerYAnchor), + trailingAnchor.constraint(equalTo: cancelButton.trailingAnchor, constant: 4), + + revealButton.widthAnchor.constraint(equalToConstant: 32), + revealButton.heightAnchor.constraint(equalToConstant: 32), + revealButton.centerYAnchor.constraint(equalTo: cancelButton.centerYAnchor), + revealButton.centerXAnchor.constraint(equalTo: cancelButton.centerXAnchor), + + restartButton.widthAnchor.constraint(equalToConstant: 32), + restartButton.heightAnchor.constraint(equalToConstant: 32), + restartButton.centerXAnchor.constraint(equalTo: revealButton.centerXAnchor), + restartButton.centerYAnchor.constraint(equalTo: revealButton.centerYAnchor), + + progressView.widthAnchor.constraint(equalToConstant: 27), + progressView.heightAnchor.constraint(equalToConstant: 27), + progressView.centerXAnchor.constraint(equalTo: cancelButton.centerXAnchor), + progressView.centerYAnchor.constraint(equalTo: cancelButton.centerYAnchor), + + separator.topAnchor.constraint(equalTo: detailLabel.bottomAnchor, constant: 12), + separator.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingAnchor.constraint(equalTo: separator.trailingAnchor), + bottomAnchor.constraint(equalTo: separator.bottomAnchor), + ]) + } + + private func subscribeToMouseOverEvents() { cancelButton.$isMouseOver.sink { [weak self] isMouseOver in self?.onButtonMouseOverChange?(isMouseOver) }.store(in: &buttonOverCancellables) @@ -163,8 +319,10 @@ final class DownloadsCellView: NSTableCellView { .store(in: &cancellables) } - private static let fileRemovedTitleAttributes: [NSAttributedString.Key: Any] = [.strikethroughStyle: 1, - .foregroundColor: NSColor.disabledControlTextColor] + private static let fileRemovedTitleAttributes: [NSAttributedString.Key: Any] = [ + .strikethroughStyle: 1, + .foregroundColor: NSColor.disabledControlTextColor + ] private func updateFilename(_ filename: String, state: DownloadViewModel.State) { // hide progress with animation on completion/failure @@ -357,3 +515,56 @@ extension DownloadsCellView.DownloadError: LocalizedError { } } + +#if DEBUG +@available(macOS 14.0, *) +#Preview { + DownloadsCellView.PreviewView() +} +@available(macOS 14.0, *) +let previewDownloadListItems = [ + DownloadListItem(identifier: .init(), added: .now, modified: .now, downloadURL: .empty, websiteURL: nil, fileName: "Indefinite progress download with long filename for clipping.zip", progress: Progress(totalUnitCount: -1), isBurner: false, destinationURL: URL(fileURLWithPath: "\(#file)"), destinationFileBookmarkData: nil, tempURL: URL(fileURLWithPath: "\(#file)"), tempFileBookmarkData: nil, error: nil), + DownloadListItem(identifier: .init(), added: .now, modified: .now, downloadURL: .empty, websiteURL: nil, fileName: "Active download.pdf", progress: Progress(totalUnitCount: 100, completedUnitCount: 42), isBurner: false, destinationURL: URL(fileURLWithPath: "\(#file)"), destinationFileBookmarkData: nil, tempURL: URL(fileURLWithPath: "\(#file)"), tempFileBookmarkData: nil, error: nil), + DownloadListItem(identifier: .init(), added: .now, modified: .now, downloadURL: .empty, websiteURL: nil, fileName: "Completed download.dmg", progress: nil, isBurner: false, destinationURL: URL(fileURLWithPath: "\(#file)"), destinationFileBookmarkData: nil, tempURL: nil, tempFileBookmarkData: nil, error: nil), + DownloadListItem(identifier: .init(), added: .now, modified: .now, downloadURL: .empty, websiteURL: nil, fileName: "Non-retryable download.txt", progress: nil, isBurner: false, destinationURL: URL(fileURLWithPath: "\(#file)"), destinationFileBookmarkData: nil, tempURL: URL(fileURLWithPath: "\(#file)"), tempFileBookmarkData: nil, error: nil), + DownloadListItem(identifier: .init(), added: .now, modified: .now, downloadURL: .empty, websiteURL: nil, fileName: "Retryable download.rtf", progress: nil, isBurner: false, destinationURL: URL(fileURLWithPath: "\(#file)"), destinationFileBookmarkData: nil, tempURL: URL(fileURLWithPath: "\(#file)"), tempFileBookmarkData: nil, error: FileDownloadError(URLError(.networkConnectionLost, userInfo: ["isRetryable": true]) as NSError)), +] +@available(macOS 14.0, *) +extension DownloadsCellView { + final class PreviewView: NSView { + + init() { + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = true + + let cells = [ + DownloadsCellView(identifier: .init("")), + DownloadsCellView(identifier: .init("")), + DownloadsCellView(identifier: .init("")), + DownloadsCellView(identifier: .init("")), + DownloadsCellView(identifier: .init("")), + ] + + for (idx, cell) in cells.enumerated() { + cell.widthAnchor.constraint(equalToConstant: 420).isActive = true + cell.heightAnchor.constraint(equalToConstant: 60).isActive = true + let item = previewDownloadListItems[idx] + cell.objectValue = DownloadViewModel(item: item) + } + + let stackView = NSStackView(views: cells as [NSView]) + stackView.orientation = .vertical + stackView.spacing = 1 + addAndLayout(stackView) + + widthAnchor.constraint(equalToConstant: 420).isActive = true + heightAnchor.constraint(equalToConstant: CGFloat((60 + 1) * cells.count)).isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + } +} +#endif diff --git a/DuckDuckGo/FileDownload/View/DownloadsPopover.swift b/DuckDuckGo/FileDownload/View/DownloadsPopover.swift index 31bc627e2f..b41bf222c3 100644 --- a/DuckDuckGo/FileDownload/View/DownloadsPopover.swift +++ b/DuckDuckGo/FileDownload/View/DownloadsPopover.swift @@ -38,7 +38,7 @@ final class DownloadsPopover: NSPopover { // swiftlint:enable force_cast private func setupContentController() { - let controller = DownloadsViewController.create() + let controller = DownloadsViewController() contentViewController = controller } diff --git a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift index dac2e0bec7..ddc8c1aeb5 100644 --- a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift +++ b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift @@ -20,58 +20,157 @@ import Cocoa import Combine protocol DownloadsViewControllerDelegate: AnyObject { - func clearDownloadsActionTriggered() - } final class DownloadsViewController: NSViewController { static let preferredContentSize = CGSize(width: 420, height: 500) - static func create() -> Self { - let storyboard = NSStoryboard(name: "Downloads", bundle: nil) - // swiftlint:disable force_cast - let controller = storyboard.instantiateInitialController() as! Self - controller.loadView() - // swiftlint:enable force_cast - return controller - } - - @IBOutlet weak var openItem: NSMenuItem! - @IBOutlet weak var showInFinderItem: NSMenuItem! - @IBOutlet weak var copyDownloadLinkItem: NSMenuItem! - @IBOutlet weak var openWebsiteItem: NSMenuItem! - @IBOutlet weak var removeFromListItem: NSMenuItem! - @IBOutlet weak var stopItem: NSMenuItem! - @IBOutlet weak var restartItem: NSMenuItem! - @IBOutlet weak var clearAllItem: NSMenuItem! - - @IBOutlet weak var titleLabel: NSTextField! + private lazy var titleLabel = NSTextField(string: UserText.downloadsDialogTitle) - @IBOutlet var openDownloadsFolderButton: NSButton! - @IBOutlet var clearDownloadsButton: NSButton! + private lazy var openDownloadsFolderButton = MouseOverButton(image: .openDownloadsFolder, target: self, action: #selector(openDownloadsFolderAction)) + private lazy var clearDownloadsButton = MouseOverButton(image: .clearDownloads, target: self, action: #selector(clearDownloadsAction)) - @IBOutlet var contextMenu: NSMenu! - @IBOutlet var tableView: NSTableView! - @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint? + private lazy var scrollView = NSScrollView() + private lazy var tableView = NSTableView() + private var tableViewHeightConstraint: NSLayoutConstraint! private var cellIndexToUnselect: Int? weak var delegate: DownloadsViewControllerDelegate? - var viewModel = DownloadListViewModel() - var downloadsCancellable: AnyCancellable? + private let viewModel: DownloadListViewModel + private var downloadsCancellable: AnyCancellable? - override func viewDidLoad() { - super.viewDidLoad() + init(viewModel: DownloadListViewModel? = nil) { + self.viewModel = viewModel ?? DownloadListViewModel() + super.init(nibName: nil, bundle: nil) + } - setupDragAndDrop() - setUpStrings() + required init?(coder: NSCoder) { + self.viewModel = DownloadListViewModel() + super.init(coder: coder) + } + override func loadView() { // swiftlint:disable:this function_body_length + view = NSView() + + view.addSubview(titleLabel) + view.addSubview(openDownloadsFolderButton) + view.addSubview(clearDownloadsButton) + view.addSubview(scrollView) + + titleLabel.isSelectable = false + titleLabel.isEditable = false + titleLabel.isBordered = false + titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + titleLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.drawsBackground = false + titleLabel.font = .preferredFont(forTextStyle: .title3) + titleLabel.textColor = .labelColor + + openDownloadsFolderButton.translatesAutoresizingMaskIntoConstraints = false + openDownloadsFolderButton.alignment = .center + openDownloadsFolderButton.bezelStyle = .shadowlessSquare + openDownloadsFolderButton.isBordered = false + openDownloadsFolderButton.imagePosition = .imageOnly + openDownloadsFolderButton.imageScaling = .scaleProportionallyDown openDownloadsFolderButton.toolTip = UserText.openDownloadsFolderTooltip + openDownloadsFolderButton.cornerRadius = 4 + openDownloadsFolderButton.backgroundInset = CGPoint(x: 2, y: 2) + openDownloadsFolderButton.normalTintColor = .button + openDownloadsFolderButton.mouseDownColor = .buttonMouseDown + openDownloadsFolderButton.mouseOverColor = .buttonMouseOver + + clearDownloadsButton.translatesAutoresizingMaskIntoConstraints = false + clearDownloadsButton.alignment = .center + clearDownloadsButton.bezelStyle = .shadowlessSquare + clearDownloadsButton.isBordered = false + clearDownloadsButton.imagePosition = .imageOnly + clearDownloadsButton.imageScaling = .scaleProportionallyDown clearDownloadsButton.toolTip = UserText.clearDownloadHistoryTooltip + clearDownloadsButton.cornerRadius = 4 + clearDownloadsButton.backgroundInset = CGPoint(x: 2, y: 2) + clearDownloadsButton.normalTintColor = .button + clearDownloadsButton.mouseDownColor = .buttonMouseDown + clearDownloadsButton.mouseOverColor = .buttonMouseOver + + scrollView.autohidesScrollers = true + scrollView.borderType = .noBorder + scrollView.hasHorizontalScroller = false + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.usesPredominantAxisScrolling = false + scrollView.automaticallyAdjustsContentInsets = false + + let clipView = NSClipView() + clipView.documentView = tableView + + clipView.autoresizingMask = [.width, .height] + clipView.drawsBackground = false + clipView.frame = CGRect(x: 0, y: 0, width: 420, height: 440) + + tableView.addTableColumn(NSTableColumn()) + + tableView.headerView = nil + tableView.backgroundColor = .clear + tableView.gridColor = .clear + tableView.style = .fullWidth + tableView.rowHeight = 60 + tableView.setContentHuggingPriority(.defaultHigh, for: .vertical) + tableView.allowsMultipleSelection = false + tableView.doubleAction = #selector(DownloadsViewController.doubleClickAction) + tableView.target = self + tableView.delegate = self + tableView.dataSource = self + tableView.menu = setUpContextMenu() + + scrollView.contentView = clipView + + let separator = NSBox() + separator.boxType = .separator + separator.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(separator) + + setupLayout(separator: separator) + } + + private func setupLayout(separator: NSBox) { + tableViewHeightConstraint = scrollView.heightAnchor.constraint(equalToConstant: 440) + + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12), + titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 12), + + openDownloadsFolderButton.widthAnchor.constraint(equalToConstant: 32), + openDownloadsFolderButton.heightAnchor.constraint(equalToConstant: 32), + openDownloadsFolderButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8), + openDownloadsFolderButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + + clearDownloadsButton.widthAnchor.constraint(equalToConstant: 32), + clearDownloadsButton.heightAnchor.constraint(equalToConstant: 32), + clearDownloadsButton.leadingAnchor.constraint(equalTo: openDownloadsFolderButton.trailingAnchor), + view.trailingAnchor.constraint(equalTo: clearDownloadsButton.trailingAnchor, constant: 11), + clearDownloadsButton.centerYAnchor.constraint(equalTo: openDownloadsFolderButton.centerYAnchor), + + view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 44), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + separator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + separator.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -2), + separator.topAnchor.constraint(equalTo: view.topAnchor, constant: 43), + + tableViewHeightConstraint + ]) + } + + override func viewDidLoad() { + super.viewDidLoad() preferredContentSize = Self.preferredContentSize + setupDragAndDrop() } override func viewWillAppear() { @@ -116,16 +215,21 @@ final class DownloadsViewController: NSViewController { downloadsCancellable = nil } - private func setUpStrings() { - titleLabel.stringValue = UserText.downloadsDialogTitle - openItem.title = UserText.downloadsOpenItem - showInFinderItem.title = UserText.downloadsShowInFinderItem - copyDownloadLinkItem.title = UserText.downloadsCopyLinkItem - openWebsiteItem.title = UserText.downloadsOpenWebsiteItem - removeFromListItem.title = UserText.downloadsRemoveFromListItem - stopItem.title = UserText.downloadsStopItem - restartItem.title = UserText.downloadsRestartItem - clearAllItem.title = UserText.downloadsClearAllItem + private func setUpContextMenu() -> NSMenu { + let menu = NSMenu { + NSMenuItem(title: UserText.downloadsOpenItem, action: #selector(openDownloadAction), target: self) + NSMenuItem(title: UserText.downloadsShowInFinderItem, action: #selector(revealDownloadAction), target: self) + NSMenuItem.separator() + NSMenuItem(title: UserText.downloadsCopyLinkItem, action: #selector(copyDownloadLinkAction), target: self) + NSMenuItem(title: UserText.downloadsOpenWebsiteItem, action: #selector(openOriginatingWebsiteAction), target: self) + NSMenuItem.separator() + NSMenuItem(title: UserText.downloadsRemoveFromListItem, action: #selector(removeDownloadAction), target: self) + NSMenuItem(title: UserText.downloadsStopItem, action: #selector(cancelDownloadAction), target: self) + NSMenuItem(title: UserText.downloadsRestartItem, action: #selector(restartDownloadAction), target: self) + NSMenuItem(title: UserText.downloadsClearAllItem, action: #selector(clearDownloadsAction), target: self) + } + menu.delegate = self + return menu } private func index(for sender: Any) -> Int? { @@ -156,7 +260,7 @@ final class DownloadsViewController: NSViewController { // MARK: User Actions - @IBAction func openDownloadsFolderAction(_ sender: Any) { + @objc func openDownloadsFolderAction(_ sender: Any) { let prefs = DownloadsPreferences.shared let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] var url: URL? @@ -197,30 +301,23 @@ final class DownloadsViewController: NSViewController { self.dismiss() } - @IBAction func clearDownloadsAction(_ sender: Any) { + @objc func clearDownloadsAction(_ sender: Any) { viewModel.cleanupInactiveDownloads() self.dismiss() delegate?.clearDownloadsActionTriggered() } - @IBAction func openDownloadedFileAction(_ sender: Any) { - guard let index = index(for: sender), - let url = viewModel.items[safe: index]?.localURL - else { return } - NSWorkspace.shared.open(url) - } - - @IBAction func cancelDownloadAction(_ sender: Any) { + @objc func cancelDownloadAction(_ sender: Any) { guard let index = index(for: sender) else { return } viewModel.cancelDownload(at: index) } - @IBAction func removeDownloadAction(_ sender: Any) { + @objc func removeDownloadAction(_ sender: Any) { guard let index = index(for: sender) else { return } viewModel.removeDownload(at: index) } - @IBAction func revealDownloadAction(_ sender: Any) { + @objc func revealDownloadAction(_ sender: Any) { guard let index = index(for: sender), let url = viewModel.items[safe: index]?.localURL else { return } @@ -228,7 +325,7 @@ final class DownloadsViewController: NSViewController { NSWorkspace.shared.activateFileViewerSelecting([url]) } - func openDownloadAction(_ sender: Any) { + @objc func openDownloadAction(_ sender: Any) { guard let index = index(for: sender), let url = viewModel.items[safe: index]?.localURL else { return } @@ -236,12 +333,12 @@ final class DownloadsViewController: NSViewController { NSWorkspace.shared.open(url) } - @IBAction func restartDownloadAction(_ sender: Any) { + @objc func restartDownloadAction(_ sender: Any) { guard let index = index(for: sender) else { return } viewModel.restartDownload(at: index) } - @IBAction func copyDownloadLinkAction(_ sender: Any) { + @objc func copyDownloadLinkAction(_ sender: Any) { guard let index = index(for: sender), let url = viewModel.items[safe: index]?.url else { return } @@ -249,7 +346,7 @@ final class DownloadsViewController: NSViewController { NSPasteboard.general.copy(url) } - @IBAction func openOriginatingWebsiteAction(_ sender: Any) { + @objc func openOriginatingWebsiteAction(_ sender: Any) { guard let index = index(for: sender), let url = viewModel.items[safe: index]?.websiteURL else { return } @@ -258,7 +355,7 @@ final class DownloadsViewController: NSViewController { WindowControllersManager.shared.show(url: url, source: .historyEntry, newTab: true) } - @IBAction func doubleClickAction(_ sender: Any) { + @objc func doubleClickAction(_ sender: Any) { if index(for: sender) != nil { openDownloadAction(sender) } else { @@ -287,7 +384,7 @@ extension DownloadsViewController: NSMenuDelegate { for menuItem in menu.items { switch menuItem.action { - case #selector(openDownloadedFileAction(_:)), + case #selector(openDownloadAction(_:)), #selector(revealDownloadAction(_:)): if case .complete(.some(let url)) = item.state, FileManager.default.fileExists(atPath: url.path) { @@ -330,20 +427,17 @@ extension DownloadsViewController: NSTableViewDataSource, NSTableViewDelegate { } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - let identifier: NSUserInterfaceItemIdentifier if viewModel.items.isEmpty { - identifier = .noDownloadsCell + return tableView.makeView(withIdentifier: .init(NoDownloadsCellView.className()), owner: self) as? NoDownloadsCellView + ?? NoDownloadsCellView(identifier: .init(NoDownloadsCellView.className())) + } else if viewModel.items.indices.contains(row) { - identifier = .downloadCell + return tableView.makeView(withIdentifier: .init(DownloadsCellView.className()), owner: self) as? DownloadsCellView + ?? DownloadsCellView(identifier: .init(DownloadsCellView.className())) } else { - identifier = .openDownloadsCell + return tableView.makeView(withIdentifier: .init(OpenDownloadsCellView.className()), owner: self) as? OpenDownloadsCellView + ?? OpenDownloadsCellView(identifier: .init(OpenDownloadsCellView.className())) } - let cell = tableView.makeView(withIdentifier: identifier, owner: nil) - if identifier == .downloadCell { - cell?.menu = contextMenu - } - - return cell } func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { @@ -388,27 +482,15 @@ extension DownloadsViewController: NSTableViewDataSource, NSTableViewDelegate { } -private extension NSUserInterfaceItemIdentifier { - static let downloadCell = NSUserInterfaceItemIdentifier(rawValue: "cell") - static let noDownloadsCell = NSUserInterfaceItemIdentifier(rawValue: "NoDownloads") - static let openDownloadsCell = NSUserInterfaceItemIdentifier(rawValue: "OpenDownloads") -} - -final class NoDownloadViewCell: NSTableCellView { - @IBOutlet weak var openFolderButton: LinkButton! - @IBOutlet weak var titleLabel: NSTextField! - - override func awakeFromNib() { - titleLabel.stringValue = UserText.downloadsNoRecentDownload - openFolderButton.title = UserText.downloadsOpenDownloadsFolder - } -} - -final class OpenDownloadViewCell: NSTableCellView { - @IBOutlet weak var openFolderButton: LinkButton! +#if DEBUG +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: DownloadsViewController.preferredContentSize.width, height: DownloadsViewController.preferredContentSize.height)) { { - override func awakeFromNib() { - openFolderButton.title = UserText.downloadsOpenDownloadsFolder + let store = DownloadListStoreMock() + store.fetchBlock = { completion in + completion(.success(previewDownloadListItems)) } - -} + let viewModel = DownloadListViewModel(coordinator: DownloadListCoordinator(store: store)) + return DownloadsViewController(viewModel: viewModel) +}() } +#endif diff --git a/DuckDuckGo/FileDownload/View/NoDownloadsCellView.swift b/DuckDuckGo/FileDownload/View/NoDownloadsCellView.swift new file mode 100644 index 0000000000..9796154c32 --- /dev/null +++ b/DuckDuckGo/FileDownload/View/NoDownloadsCellView.swift @@ -0,0 +1,82 @@ +// +// NoDownloadsCellView.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 AppKit + +final class NoDownloadsCellView: NSTableCellView { + + fileprivate enum Constants { + static let width: CGFloat = 420 + static let height: CGFloat = 60 + } + + private let titleLabel = NSTextField(string: UserText.downloadsNoRecentDownload) + private let openFolderButton = LinkButton(title: UserText.downloadsOpenDownloadsFolder, + target: nil, + action: #selector(DownloadsViewController.openDownloadsFolderAction)) + + init(identifier: NSUserInterfaceItemIdentifier) { + super.init(frame: CGRect(x: 0, y: 0, width: Constants.width, height: Constants.height)) + self.identifier = identifier + + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + private func setupUI() { + addSubview(titleLabel) + addSubview(openFolderButton) + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.isEditable = false + titleLabel.isBordered = false + titleLabel.isSelectable = false + titleLabel.drawsBackground = false + titleLabel.font = .systemFont(ofSize: 13) + titleLabel.textColor = .secondaryLabelColor + titleLabel.lineBreakMode = .byTruncatingMiddle + + openFolderButton.translatesAutoresizingMaskIntoConstraints = false + openFolderButton.bezelStyle = .shadowlessSquare + openFolderButton.isBordered = false + openFolderButton.alignment = .center + openFolderButton.font = .systemFont(ofSize: 13) + openFolderButton.contentTintColor = .linkColor + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12), + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + + openFolderButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, + constant: 4), + openFolderButton.centerXAnchor.constraint(equalTo: centerXAnchor), + ]) + } + +} + +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: NoDownloadsCellView.Constants.width, + height: NoDownloadsCellView.Constants.height)) { + PreviewViewController(showWindowTitle: false) { + NoDownloadsCellView(identifier: .init("")) + } +} diff --git a/DuckDuckGo/FileDownload/View/OpenDownloadsCellView.swift b/DuckDuckGo/FileDownload/View/OpenDownloadsCellView.swift new file mode 100644 index 0000000000..fbfed592da --- /dev/null +++ b/DuckDuckGo/FileDownload/View/OpenDownloadsCellView.swift @@ -0,0 +1,67 @@ +// +// OpenDownloadsCellView.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 AppKit + +final class OpenDownloadsCellView: NSTableCellView { + + fileprivate enum Constants { + static let width: CGFloat = 420 + static let height: CGFloat = 60 + } + + private let openFolderButton = LinkButton(title: UserText.downloadsOpenDownloadsFolder, + target: nil, + action: #selector(DownloadsViewController.openDownloadsFolderAction)) + + init(identifier: NSUserInterfaceItemIdentifier) { + super.init(frame: CGRect(x: 0, y: 0, width: Constants.width, height: Constants.height)) + self.identifier = identifier + + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + private func setupUI() { + addSubview(openFolderButton) + + openFolderButton.translatesAutoresizingMaskIntoConstraints = false + openFolderButton.bezelStyle = .shadowlessSquare + openFolderButton.isBordered = false + openFolderButton.alignment = .center + openFolderButton.font = .systemFont(ofSize: 13) + openFolderButton.contentTintColor = .linkColor + + NSLayoutConstraint.activate([ + openFolderButton.centerYAnchor.constraint(equalTo: centerYAnchor), + openFolderButton.centerXAnchor.constraint(equalTo: centerXAnchor), + ]) + } + +} + +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: OpenDownloadsCellView.Constants.width, + height: OpenDownloadsCellView.Constants.height)) { + PreviewViewController(showWindowTitle: false) { + OpenDownloadsCellView(identifier: .init("")) + } +} diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 87e0f9dfb4..04f8d61b80 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -14664,66 +14664,6 @@ } } }, - "downloads.restart.item" : { - "comment" : "Contextual menu item in downloads manager to restart the download", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stopp" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Stop" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Detener" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Arrêter" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Interrompi" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stoppen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zatrzymaj" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Parar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Остановить" - } - } - } - }, "downloads.show-in-finder.item" : { "comment" : "Contextual menu item in downloads manager to show the downloaded file in Finder", "extractionState" : "extracted_with_value", From fd879bf9e392e5db4583c7349ef144135fee1ebe Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Wed, 10 Apr 2024 17:28:50 +0200 Subject: [PATCH 060/221] Fix lottie high Windowserver load (#2595) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207024603216659/f **Description**: Force use of .mainThread to prevent high WindowServer Usage (Pending Fix with newer Lottie versions) --- DuckDuckGo/Application/AppDelegate.swift | 6 ++++++ .../View/AddressBarButtonsViewController.swift | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 22cc5ea79e..53ac49ff5b 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -222,6 +222,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PrivacyFeatures.httpsUpgrade.loadDataAsync() bookmarksManager.loadBookmarks() + + // Force use of .mainThread to prevent high WindowServer Usage + // Pending Fix with newer Lottie versions + // https://app.asana.com/0/1177771139624306/1207024603216659/f + LottieConfiguration.shared.renderingEngine = .mainThread + if case .normal = NSApp.runType { FaviconManager.shared.loadFavicons() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 7bb06c097f..148900fcec 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -553,7 +553,13 @@ final class AddressBarButtonsViewController: NSViewController { } private func setupAnimationViews() { - func addAndLayoutAnimationViewIfNeeded(animationView: LottieAnimationView?, animationName: String, renderingEngine: Lottie.RenderingEngineOption = .automatic) -> LottieAnimationView { + + func addAndLayoutAnimationViewIfNeeded(animationView: LottieAnimationView?, + animationName: String, + // Default use of .mainThread to prevent high WindowServer Usage + // Pending Fix with newer Lottie versions + // https://app.asana.com/0/1177771139624306/1207024603216659/f + renderingEngine: Lottie.RenderingEngineOption = .mainThread) -> LottieAnimationView { if let animationView = animationView, animationView.identifier?.rawValue == animationName { return animationView } From 69680b0ea495f7c5d6fe7c2866e4cdd6f71fd80b Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 10 Apr 2024 18:34:47 +0200 Subject: [PATCH 061/221] Fix VPN bug: Nearest city breaks register requests (#2589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1203137811378537/1207044169885477/f **Description**: Due to a bug in the VPNLocationViewModel, we were using the string “Nearest” as a city ID which ended up making it to the `register` request. This only happens when we explicitly select Nearest rather than just selecting the country (which has the same effect). **Steps to test this PR**: 1. Make sure you’re hitting the production VPN environment (so you get the full countries + cities). 2. Go to VPN Location screen 3. Go to a country with multiple cities (currently only the US) and select any specific city 4. Now select Nearest (explicitly using the drop-down) 5. Go to the VPN management popover (e.g nav bar → … → VPN) and connect **Expected** VPN connects to the nearest city in that country --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../BothAppTargets/VPNLocation/VPNLocationViewModel.swift | 3 ++- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2bbc99a10b..43f4bd6b34 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14448,7 +14448,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 133.0.0; + version = "133.0.0-1"; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 46c657c96d..8ab89dd501 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "c0b0cb55e7ac2f69d10452e1a5c06713155d798e", - "version" : "133.0.0" + "revision" : "ab719a6a786a3c4e898496c13ca76a470e89893b", + "version" : "133.0.0-1" } }, { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index 01a39e35f4..3368da5066 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -80,7 +80,8 @@ final class VPNLocationViewModel: ObservableObject { func onCountryItemSelection(id: String, cityId: String? = nil) async { DailyPixel.fire(pixel: .networkProtectionGeoswitchingSetCustom, frequency: .dailyAndCount) - let location = NetworkProtectionSelectedLocation(country: id, city: cityId) + let city = cityId == VPNCityItemModel.nearest.id ? nil : cityId + let location = NetworkProtectionSelectedLocation(country: id, city: city) selectedLocation = .location(location) await reloadList() } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 46c094664b..78a7506d5e 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", exact: "133.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0-1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index d0ea0235c3..9667eea790 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0-1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 556d4f308e..808b065230 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: "133.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.0-1"), .package(path: "../SwiftUIExtensions") ], targets: [ From 7b1103ab2bd6a852b2d841ef41e5316a95383c8a Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 10 Apr 2024 19:22:44 +0200 Subject: [PATCH 062/221] BSK release 133.1.0 (#2597) Task/Issue URL: https://app.asana.com/0/414235014887631/1207044067978093/f **Description**: **Steps to test this PR**: 1. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 415aeb2e28..d002b6758e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14496,7 +14496,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 133.0.1; + version = 133.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a575d944d1..517700a5d4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "39d74829150a9ecffea2f503c01851e54eda8ad1", - "version" : "133.0.1" + "revision" : "4699a5ff3d0669736e87f6da808884f245d80ede", + "version" : "133.1.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 9418e01778..1c0d592717 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", exact: "133.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 4e62507c16..08e64e721c 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 1c064eb962..c5720e3b75 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: "133.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From b44fa7f7824793d5c8edb74395a6927da762c0f9 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 11 Apr 2024 03:04:07 +0000 Subject: [PATCH 063/221] Bump version to 1.83.0 (158) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index eb40c52354..9cff20393f 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 157 +CURRENT_PROJECT_VERSION = 158 From 01d4705fc2ee2ecc189b31a65f06ca061df063e5 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 10 Apr 2024 21:35:26 -0700 Subject: [PATCH 064/221] [Release PR] Fix lottie high Windowserver load (#2598) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207024603216659/f Description: This PR cherry pick's @afterxleep's changes to fix Lottie CPU usage. (#2595) --- DuckDuckGo/Application/AppDelegate.swift | 6 ++++++ .../View/AddressBarButtonsViewController.swift | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 22cc5ea79e..53ac49ff5b 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -222,6 +222,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PrivacyFeatures.httpsUpgrade.loadDataAsync() bookmarksManager.loadBookmarks() + + // Force use of .mainThread to prevent high WindowServer Usage + // Pending Fix with newer Lottie versions + // https://app.asana.com/0/1177771139624306/1207024603216659/f + LottieConfiguration.shared.renderingEngine = .mainThread + if case .normal = NSApp.runType { FaviconManager.shared.loadFavicons() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 7afc044118..8d28152841 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -553,7 +553,13 @@ final class AddressBarButtonsViewController: NSViewController { } private func setupAnimationViews() { - func addAndLayoutAnimationViewIfNeeded(animationView: LottieAnimationView?, animationName: String, renderingEngine: Lottie.RenderingEngineOption = .automatic) -> LottieAnimationView { + + func addAndLayoutAnimationViewIfNeeded(animationView: LottieAnimationView?, + animationName: String, + // Default use of .mainThread to prevent high WindowServer Usage + // Pending Fix with newer Lottie versions + // https://app.asana.com/0/1177771139624306/1207024603216659/f + renderingEngine: Lottie.RenderingEngineOption = .mainThread) -> LottieAnimationView { if let animationView = animationView, animationView.identifier?.rawValue == animationName { return animationView } From 33331a1093c82a881bb3250e4f2a35adde422d85 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 11 Apr 2024 05:13:12 +0000 Subject: [PATCH 065/221] Bump version to 1.83.0 (159) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 9cff20393f..7ac22f18ce 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 158 +CURRENT_PROJECT_VERSION = 159 From bf6d92e62e7c32c3e6f9d77fc7e0dadcd4b4824e Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 11 Apr 2024 11:09:27 +0100 Subject: [PATCH 066/221] Update copy for DBP open button (#2586) Task/Issue URL: https://app.asana.com/0/1204167627774280/1207034788876871/f **Description**: Update copy for DBP open button --- .../SubscriptionUI/Sources/SubscriptionUI/UserText.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index fed703c3f7..645253c3a4 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -30,7 +30,7 @@ enum UserText { static let personalInformationRemovalServiceTitle = NSLocalizedString("subscription.preferences.services.personal.information.removal.title", value: "Personal Information Removal", comment: "Title for the Personal Information Removal service listed in the subscription preferences pane") static let personalInformationRemovalServiceDescription = NSLocalizedString("subscription.preferences.services.personal.information.removal.description", value: "Find and remove your personal information from sites that store and sell it.", comment: "Description for the Personal Information Removal service listed in the subscription preferences pane") - static let personalInformationRemovalServiceButtonTitle = NSLocalizedString("subscription.preferences.services.personal.information.removal.button.title", value: "Get Started", comment: "Title for the Personal Information Removal service button to open its settings") + static let personalInformationRemovalServiceButtonTitle = NSLocalizedString("subscription.preferences.services.personal.information.removal.button.title", value: "Open", comment: "Title for the Personal Information Removal service button to open its settings") static let identityTheftRestorationServiceTitle = NSLocalizedString("subscription.preferences.services.identity.theft.restoration.title", value: "Identity Theft Restoration", comment: "Title for the Identity Theft Restoration service listed in the subscription preferences pane") static let identityTheftRestorationServiceDescription = NSLocalizedString("subscription.preferences.services.identity.theft.restoration.description", value: "Restore stolen accounts and financial losses in the event of identity theft.", comment: "Description for the Identity Theft Restoration service listed in the subscription preferences pane") From 99b8fab97c4e02a96bdc9b04416f8382efd89023 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Thu, 11 Apr 2024 16:32:34 +0200 Subject: [PATCH 067/221] Automatically mark / close stale PRs (#2596) Task/Issue URL: https://app.asana.com/0/414709148257752/1207048535807038/f **Description**: - Marks inactive PRs as 'stale' after 7 days - Closes inactive PRs after 14 days --- .github/workflows/stale_pr.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/stale_pr.yml diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml new file mode 100644 index 0000000000..a5590c9c63 --- /dev/null +++ b/.github/workflows/stale_pr.yml @@ -0,0 +1,19 @@ +name: Close Stale Pull Requests + +on: + schedule: + - cron: '0 0 * * *' + +jobs: + close_stale_prs: + runs-on: ubuntu-latest + steps: + - name: Close stale pull requests + uses: actions/stale@v9 + with: + stale-pr-message: 'This PR has been inactive for more than 7 days and will be automatically closed 7 days from now.' + days-before-stale: 7 + close-pr-message: 'This PR has been closed after 14 days of inactivity. Feel free to reopen it if you plan to continue working on it or have further discussions.' + days-before-close: 7 + stale-pr-label: stale + exempt-draft-pr: true \ No newline at end of file From b5796b624536c73d6f58d81f30ba907be80a6813 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:28:20 -0400 Subject: [PATCH 068/221] Fix popover not displayed reliably when VPN shortcut is unpinned (#2606) Task/Issue URL: https://app.asana.com/0/1203137811378537/1207059171151302/f Tech Design URL: CC: Description: When the VPN shortcut is unpinned & the user tries to open the popover from the More Options menu, the temporarily shown VPN shortcut is hidden prematurely and (?) cause the popover to be closed right away. --- DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 0274af6b08..8c99ed1284 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -973,6 +973,7 @@ extension NavigationBarViewController: NSMenuDelegate { .store(in: &cancellables) networkProtectionButtonModel.$showButton + .removeDuplicates() .receive(on: RunLoop.main) .sink { [weak self] show in let isPopUpWindow = self?.view.window?.isPopUpWindow ?? false From ad5947f4119b0c9bacc59afc21b7954a144c2f99 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 11 Apr 2024 23:51:05 +0200 Subject: [PATCH 069/221] macOS VPN: Ask users to reboot if system extension was not uninstalled (#2603) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207056555335340/f ## Description We now ask again users to reboot if macOS requires it to complete installation of the system extension. --- .../Common/Localizables/UserText+NetworkProtection.swift | 2 +- .../NetworkProtectionPixelEvent.swift | 8 ++++---- .../NetworkProtectionTunnelController.swift | 6 +++--- DuckDuckGoVPN/NetworkExtensionController.swift | 2 +- .../NetworkProtectionPixelEventTests.swift | 6 ++++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 69d36bb228..2a5ec04f2d 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -67,7 +67,7 @@ extension UserText { // "network.protection.system.extension.unknown.activation.error" - Message shown to users when they try to enable NetP and there is an unexpected activation error. static let networkProtectionUnknownActivationError = "There as an unexpected error. Please try again." // "network.protection.system.extension.please.reboot" - Message shown to users when they try to enable NetP and they need to reboot the computer to complete the installation - static let networkProtectionPleaseReboot = "Please reboot to activate the VPN" + static let networkProtectionPleaseReboot = "VPN update available. Restart your Mac to reconnect." } // MARK: - VPN Waitlist diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 890f942b93..d1797d0e0c 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -85,7 +85,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionRekeyCompleted case networkProtectionRekeyFailure(_ error: Error) - case networkProtectionSystemExtensionActivationFailure + case networkProtectionSystemExtensionActivationFailure(_ error: Error) case networkProtectionUnhandledError(function: String, line: Int, error: Error) @@ -393,8 +393,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionWireguardErrorCannotStartWireguardBackend, .networkProtectionNoAuthTokenFoundError, .networkProtectionRekeyAttempt, - .networkProtectionRekeyCompleted, - .networkProtectionSystemExtensionActivationFailure: + .networkProtectionRekeyCompleted: return nil case .networkProtectionClientFailedToRedeemInviteCode(let error), .networkProtectionClientFailedToFetchLocations(let error), @@ -408,7 +407,8 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionClientFailedToParseRedeemResponse(let error), .networkProtectionWireguardErrorCannotSetNetworkSettings(let error), .networkProtectionRekeyFailure(let error), - .networkProtectionUnhandledError(_, _, let error): + .networkProtectionUnhandledError(_, _, let error), + .networkProtectionSystemExtensionActivationFailure(let error): return error } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index e42b917710..911913504f 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -418,16 +418,16 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr controllerErrorStore.lastErrorMessage = UserText.networkProtectionSystemSettings case SystemExtensionRequestError.unknownRequestResult: controllerErrorStore.lastErrorMessage = UserText.networkProtectionUnknownActivationError - case SystemExtensionRequestError.willActivateAfterReboot: + case OSSystemExtensionError.extensionNotFound, + SystemExtensionRequestError.willActivateAfterReboot: controllerErrorStore.lastErrorMessage = UserText.networkProtectionPleaseReboot default: controllerErrorStore.lastErrorMessage = error.localizedDescription } PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, + NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure(error), frequency: .standard, - withError: error, includeAppVersionParameter: true ) diff --git a/DuckDuckGoVPN/NetworkExtensionController.swift b/DuckDuckGoVPN/NetworkExtensionController.swift index d850bfbb80..23a5eca958 100644 --- a/DuckDuckGoVPN/NetworkExtensionController.swift +++ b/DuckDuckGoVPN/NetworkExtensionController.swift @@ -54,7 +54,7 @@ extension NetworkExtensionController { NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = extensionVersion - try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC) + try await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC) #endif } diff --git a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift index 43ccdb22ab..a2209cd64b 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift +++ b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift @@ -303,8 +303,10 @@ final class NetworkProtectionPixelEventTests: XCTestCase { underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) - fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, - and: .expect(pixelName: "m_mac_netp_system_extension_activation_failure"), + fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure(TestError.testError), + and: .expect(pixelName: "m_mac_netp_system_extension_activation_failure", + error: TestError.testError, + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionUnhandledError(function: "function", line: 1, error: TestError.testError), From 53bcda32d8e59e889084bb81cc14dbaf95aa3c55 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 11 Apr 2024 23:51:05 +0200 Subject: [PATCH 070/221] macOS VPN: Ask users to reboot if system extension was not uninstalled (#2603) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207056555335340/f ## Description We now ask again users to reboot if macOS requires it to complete installation of the system extension. --- .../Common/Localizables/UserText+NetworkProtection.swift | 2 +- .../NetworkProtectionPixelEvent.swift | 8 ++++---- .../NetworkProtectionTunnelController.swift | 6 +++--- DuckDuckGoVPN/NetworkExtensionController.swift | 2 +- .../NetworkProtectionPixelEventTests.swift | 6 ++++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 69d36bb228..2a5ec04f2d 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -67,7 +67,7 @@ extension UserText { // "network.protection.system.extension.unknown.activation.error" - Message shown to users when they try to enable NetP and there is an unexpected activation error. static let networkProtectionUnknownActivationError = "There as an unexpected error. Please try again." // "network.protection.system.extension.please.reboot" - Message shown to users when they try to enable NetP and they need to reboot the computer to complete the installation - static let networkProtectionPleaseReboot = "Please reboot to activate the VPN" + static let networkProtectionPleaseReboot = "VPN update available. Restart your Mac to reconnect." } // MARK: - VPN Waitlist diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift index 890f942b93..d1797d0e0c 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/NetworkProtectionPixelEvent.swift @@ -85,7 +85,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { case networkProtectionRekeyCompleted case networkProtectionRekeyFailure(_ error: Error) - case networkProtectionSystemExtensionActivationFailure + case networkProtectionSystemExtensionActivationFailure(_ error: Error) case networkProtectionUnhandledError(function: String, line: Int, error: Error) @@ -393,8 +393,7 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionWireguardErrorCannotStartWireguardBackend, .networkProtectionNoAuthTokenFoundError, .networkProtectionRekeyAttempt, - .networkProtectionRekeyCompleted, - .networkProtectionSystemExtensionActivationFailure: + .networkProtectionRekeyCompleted: return nil case .networkProtectionClientFailedToRedeemInviteCode(let error), .networkProtectionClientFailedToFetchLocations(let error), @@ -408,7 +407,8 @@ enum NetworkProtectionPixelEvent: PixelKitEventV2 { .networkProtectionClientFailedToParseRedeemResponse(let error), .networkProtectionWireguardErrorCannotSetNetworkSettings(let error), .networkProtectionRekeyFailure(let error), - .networkProtectionUnhandledError(_, _, let error): + .networkProtectionUnhandledError(_, _, let error), + .networkProtectionSystemExtensionActivationFailure(let error): return error } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index e42b917710..911913504f 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -418,16 +418,16 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr controllerErrorStore.lastErrorMessage = UserText.networkProtectionSystemSettings case SystemExtensionRequestError.unknownRequestResult: controllerErrorStore.lastErrorMessage = UserText.networkProtectionUnknownActivationError - case SystemExtensionRequestError.willActivateAfterReboot: + case OSSystemExtensionError.extensionNotFound, + SystemExtensionRequestError.willActivateAfterReboot: controllerErrorStore.lastErrorMessage = UserText.networkProtectionPleaseReboot default: controllerErrorStore.lastErrorMessage = error.localizedDescription } PixelKit.fire( - NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, + NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure(error), frequency: .standard, - withError: error, includeAppVersionParameter: true ) diff --git a/DuckDuckGoVPN/NetworkExtensionController.swift b/DuckDuckGoVPN/NetworkExtensionController.swift index d850bfbb80..23a5eca958 100644 --- a/DuckDuckGoVPN/NetworkExtensionController.swift +++ b/DuckDuckGoVPN/NetworkExtensionController.swift @@ -54,7 +54,7 @@ extension NetworkExtensionController { NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = extensionVersion - try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC) + try await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC) #endif } diff --git a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift index 43ccdb22ab..a2209cd64b 100644 --- a/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift +++ b/UnitTests/NetworkProtection/NetworkProtectionPixelEventTests.swift @@ -303,8 +303,10 @@ final class NetworkProtectionPixelEventTests: XCTestCase { underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) - fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure, - and: .expect(pixelName: "m_mac_netp_system_extension_activation_failure"), + fire(NetworkProtectionPixelEvent.networkProtectionSystemExtensionActivationFailure(TestError.testError), + and: .expect(pixelName: "m_mac_netp_system_extension_activation_failure", + error: TestError.testError, + underlyingErrors: [TestError.underlyingError]), file: #filePath, line: #line) fire(NetworkProtectionPixelEvent.networkProtectionUnhandledError(function: "function", line: 1, error: TestError.testError), From baad13ea74a594bdf9f03fe732e5137090d74f01 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 11 Apr 2024 22:14:27 +0000 Subject: [PATCH 071/221] Bump version to 1.83.0 (160) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 7ac22f18ce..937777042c 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 159 +CURRENT_PROJECT_VERSION = 160 From 13d488eaa900c56a7f57f8ba020dfe5a5d2df7b4 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 11 Apr 2024 20:38:28 -0700 Subject: [PATCH 072/221] Update metadata for Privacy Pro release (#2609) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207061219313294/f Tech Design URL: CC: **Description**: This PR updates metadata in the repo for Privacy Pro. **Steps to test this PR**: 1. Check that copy is correct; for bonus points, verify that it matches the metadata delivered in [this task](https://app.asana.com/0/0/1206830640705812/f) --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- fastlane/metadata/en-US/description.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fastlane/metadata/en-US/description.txt b/fastlane/metadata/en-US/description.txt index d97590ce20..b2f7b6c6d6 100644 --- a/fastlane/metadata/en-US/description.txt +++ b/fastlane/metadata/en-US/description.txt @@ -26,6 +26,15 @@ EVERYDAY PRIVACY CONTROLS  • Banish cookie pop-ups and automatically set your preferences to minimize cookies and maximize privacy.    • Signal your privacy preference with Global Privacy Control (GPC) built into our app. GPC intends to help you express your opt-out rights automatically by telling websites not to sell or share your personal info. Whether it can be used to enforce your legal rights depends on the laws in your jurisdiction.  + +PRIVACY PRO  +Subscribe to Privacy Pro for:  +  +• Our VPN: Secure your connection on up to 5 devices.  + +• Personal Information Removal: Find and remove personal info from sites that store and sell it. + +• Identity Theft Restoration: If your identity is stolen, we’ll help restore it.    Privacy Pro Pricing & Terms From 22cddbcc0d058a150b339a08334d86ffec5e770f Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:45:38 -0400 Subject: [PATCH 073/221] Open new browser window if no windows exist (#2610) Task/Issue URL: https://app.asana.com/0/1203137811378537/1207061568595472/f Tech Design URL: CC: **Description**: Fixes an issue when "Open DDG" doesn't create a new window if none existing window is found. **Steps to test this PR**: 1. With at least one DDG opened and in the background, open status bar menu > Open DDG 2. Expect: DDG is brought to the foreground 3. Close all DDG windows 4. Open status bar menu > Open DDG 5. Expect: New DDG window is created --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo/Application/URLEventHandler.swift | 2 ++ DuckDuckGo/Windows/View/WindowControllersManager.swift | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index e261efdf12..696d2b59d7 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -145,6 +145,8 @@ final class URLEventHandler { WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) case AppLaunchCommand.shareFeedback.launchURL: WindowControllersManager.shared.showShareFeedbackModal() + case AppLaunchCommand.justOpen.launchURL: + WindowControllersManager.shared.showNewWindow() case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index ae37e64c41..f28e90c26b 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -237,6 +237,13 @@ extension WindowControllersManager { } } + func showNewWindow() { + guard WindowControllersManager.shared.lastKeyMainWindowController == nil else { return } + let tabCollection = TabCollection(tabs: []) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + _ = WindowsManager.openNewWindow(with: tabCollectionViewModel) + } + func showLocationPickerSheet() { let locationsViewController = VPNLocationsHostingViewController() let locationsWindowController = locationsViewController.wrappedInWindowController() From 5da74bf1f67bdd2b8f5fe885a631d529fba6bf68 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 11 Apr 2024 21:23:37 -0700 Subject: [PATCH 074/221] Remove timezone offset from the VPN server object (#2580) Task/Issue URL: https://app.asana.com/0/414235014887631/1207032029127388/f Tech Design URL: CC: Description: This PR removes support for tzOffset, which is no longer used. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d002b6758e..f4ef350498 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14496,7 +14496,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 133.1.0; + version = 134.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 517700a5d4..403ec8030a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "4699a5ff3d0669736e87f6da808884f245d80ede", - "version" : "133.1.0" + "revision" : "bc70d1a27263cc97a4060ac9e73ec10929c28a29", + "version" : "134.0.0" } }, { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 1c0d592717..0cf9ebe7c0 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", exact: "133.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 08e64e721c..7b5cbdc07d 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "133.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index c5720e3b75..d84099b47d 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: "133.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From e13e14d91a8f3ec987fbf7e6b83138e41b2ae317 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 12 Apr 2024 05:12:13 +0000 Subject: [PATCH 075/221] Bump version to 1.83.0 (161) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 937777042c..3e6bef97fd 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 160 +CURRENT_PROJECT_VERSION = 161 From 209a4254dd69cad18ccf48c77fd70300f6d96ebf Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 12 Apr 2024 09:42:22 +0100 Subject: [PATCH 076/221] Fix secure coding error (#2604) Task/Issue URL: https://app.asana.com/0/1203581873609357/1207056719863102/f **Description**: Fix issue sending XPC messages on error --- .../DataBrokerProtectionScheduler.swift | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index a6e5f15362..1c2cebe3eb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -28,12 +28,17 @@ public enum DataBrokerProtectionSchedulerStatus: Codable { } @objc -public class DataBrokerProtectionSchedulerErrorCollection: NSObject { +public class DataBrokerProtectionSchedulerErrorCollection: NSObject, NSSecureCoding { /* This needs to be an NSObject (rather than a struct) so it can be represented in Objective C - for the IPC layer + and confrom to NSSecureCoding for the IPC layer. */ + private enum NSSecureCodingKeys { + static let oneTimeError = "oneTimeError" + static let operationErrors = "operationErrors" + } + public let oneTimeError: Error? public let operationErrors: [Error]? @@ -42,6 +47,22 @@ public class DataBrokerProtectionSchedulerErrorCollection: NSObject { self.operationErrors = operationErrors super.init() } + + // MARK: - NSSecureCoding + + public static var supportsSecureCoding: Bool { + return true + } + + public func encode(with coder: NSCoder) { + coder.encode(oneTimeError, forKey: NSSecureCodingKeys.oneTimeError) + coder.encode(operationErrors, forKey: NSSecureCodingKeys.operationErrors) + } + + public required init?(coder: NSCoder) { + oneTimeError = coder.decodeObject(of: NSError.self, forKey: NSSecureCodingKeys.oneTimeError) + operationErrors = coder.decodeArrayOfObjects(ofClass: NSError.self, forKey: NSSecureCodingKeys.operationErrors) + } } public protocol DataBrokerProtectionScheduler { From a9637fcd215452e88a365d05b2a761867f68b186 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 12 Apr 2024 12:39:07 +0200 Subject: [PATCH 077/221] Remove obsolete second subscription and entitlement refresh call (#2613) Task/Issue URL: https://app.asana.com/0/1203936086921904/1207025896069381/f **Description**: The following call is obsolete as subscription and entitlement refresh is already handled AppDelegate. Additionally refreshSubscriptionAndEntitlements() method is deprecated and going to be removed as it has baked in side effect of signing out user when the subscription is found expired. **Steps to test this PR**: 1. Purchase or activate subscription 2. Make app inactive and active again 3. Ensure that there are singular calls to auth and subscription endpoints for fetching entitlements and subscription infor --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../BothAppTargets/NetworkProtectionAppEvents.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index 89aa046691..25be11ba61 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -81,10 +81,6 @@ final class NetworkProtectionAppEvents { func applicationDidBecomeActive() { Task { @MainActor in await featureVisibility.disableIfUserHasNoAccess() - -#if SUBSCRIPTION - await AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).refreshSubscriptionAndEntitlements() -#endif } } From bab362b8fd14e63430a4959ca45048874c1c3fe1 Mon Sep 17 00:00:00 2001 From: Halle <378795+Halle@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:42:13 -0700 Subject: [PATCH 078/221] Adds a series of UI tests for Address Bar Keyboard Task/Issue URL: https://app.asana.com/0/1199230911884351/1207045927838862/f Tech Design URL: CC: **Description**: Adds a series of UI tests for Address Bar Keyboard Shortcuts **Steps to test this PR**: 1. Open the scheme **UI Tests** 2. Navigate to the test pane 3. Run `AddressBarKeyboardShortcutsTests` **UI Tests general guidelines for everyone**: * It isn't possible to multitask while running UI tests on your local machine, because your mousing/interface usage or the unknown focus events of other in-use applications can change, slow down, or intercept screen interactions that the tests depend on. Multitasking includes being on calls, and includes work in a different space. Tedious as it is, the UI tests are the only thing you can do with your computer when you test whether UI tests pass, because that is the only circumstance that `XCUIAutomation` effectively supports locally (this makes sense if you consider how closed the simulator environment where this API gets the most development is, by contrast with an engineer's development computer). This approach keeps the debug loop fast and the tests free of workaround buildup. * Due to app-specific text field focus issues related to waiting for focus across two monitors simultaneously, we've decided to review the tests on a single monitor. * English is the currently supported language for UI tests * The tests have been tested on multiple systems before the PR is opened. If you experience a failure when testing locally, in order to confirm that it isn't a false negative failure, please take the following steps before reporting it: 1. If you weren't watching when it happened (probably not, it's pretty tedious) please re-run the failed case and observe the failure. This does a few things: it means the person most acquainted with the system where the test failed can describe what they observed and knows what the test tries to do, which will promote a faster and more minimal solution, and it is a way of ruling out accidental multitasking, which can happen if a call comes in or a long GUI process elsewhere concludes. If a failure recurs when you observe the test case, please report it. 2. When reporting it, please send the `xcresult` bundle, even if it seems like the same failure as a previous failure (it may be subtly different). Since the `xcresult` bundle will include a screen recording or screenshots, for your privacy, please consider the visibility of things in your system you may not want to send to a contractor when you are running the tests. Thank you very much for your help! --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../xcshareddata/xcschemes/UI Tests.xcscheme | 2 +- DuckDuckGo/Menus/MainMenu.swift | 2 +- .../View/PreferencesGeneralView.swift | 2 +- .../AddressBarKeyboardShortcutsTests.swift | 268 ++++++++++++++++++ UITests/AutocompleteTests.swift | 4 + UITests/Common/UITests.swift | 39 +++ 7 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 UITests/AddressBarKeyboardShortcutsTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4ef350498..bafb4c7c68 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3345,6 +3345,7 @@ EEC589DA2A4F1CE400BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */; }; + EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC7BE2D2BC6C09400F86835 /* AddressBarKeyboardShortcutsTests.swift */; }; EEC8EB3E2982CA3B0065AA39 /* JSAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC111E5294D06290086524F /* JSAlertViewModel.swift */; }; EEC8EB3F2982CA440065AA39 /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; @@ -4808,6 +4809,7 @@ EEC4A6682B2C87D300F7C0AA /* VPNLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationView.swift; sourceTree = ""; }; EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItemModel.swift; sourceTree = ""; }; EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItem.swift; sourceTree = ""; }; + EEC7BE2D2BC6C09400F86835 /* AddressBarKeyboardShortcutsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressBarKeyboardShortcutsTests.swift; sourceTree = ""; }; EECE10E429DD77E60044D027 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; EED735352BB46B6000F173D6 /* AutocompleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteTests.swift; sourceTree = ""; }; EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+VPNAgentConvenienceInitializers.swift"; sourceTree = ""; }; @@ -6579,6 +6581,7 @@ isa = PBXGroup; children = ( EEBCE6802BA444FA00B9DF00 /* Common */, + EEC7BE2D2BC6C09400F86835 /* AddressBarKeyboardShortcutsTests.swift */, EED735352BB46B6000F173D6 /* AutocompleteTests.swift */, EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */, EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */, @@ -12318,6 +12321,7 @@ EE0429E02BA31D2F009EB20F /* FindInPageTests.swift in Sources */, EE02D4212BB460FE00DBE6B3 /* StringExtension.swift in Sources */, EE9D81C32BC57A3700338BE3 /* StateRestorationTests.swift in Sources */, + EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */, EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */, EE02D4222BB4611A00DBE6B3 /* TestsURLExtension.swift in Sources */, 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme index 5ded4d9621..eea4f50141 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/UI Tests.xcscheme @@ -33,7 +33,7 @@ ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> + scriptText = ""${PROJECT_DIR}/clean-app.sh" review defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 killall tests-server # integration tests resources dir pushd "${METAL_LIBRARY_OUTPUT_DIR}" "${BUILT_PRODUCTS_DIR}/tests-server" & popd "> NSMenu { let debugMenu = NSMenu(title: "Debug") { - NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)) + NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() NSMenuItem(title: "Reset Data") { NSMenuItem(title: "Reset Default Browser Prompt", action: #selector(MainViewController.resetDefaultBrowserPrompt)) diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index dc4da53438..ad54e6e6ac 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -95,7 +95,7 @@ extension Preferences { // SECTION 3: Search Settings PreferencePaneSection(UserText.privateSearch) { - ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions) + ToggleMenuItem(UserText.showAutocompleteSuggestions, isOn: $searchModel.showAutocompleteSuggestions).accessibilityIdentifier("PreferencesGeneralView.showAutocompleteSuggestions") } // SECTION 4: Downloads diff --git a/UITests/AddressBarKeyboardShortcutsTests.swift b/UITests/AddressBarKeyboardShortcutsTests.swift new file mode 100644 index 0000000000..675b4f63b3 --- /dev/null +++ b/UITests/AddressBarKeyboardShortcutsTests.swift @@ -0,0 +1,268 @@ +// +// AddressBarKeyboardShortcutsTests.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 XCTest + +class AddressBarKeyboardShortcutsTests: XCTestCase { + private var app: XCUIApplication! + private var urlStringForAddressBar: String! + private var urlForAddressBar: URL! + + private var addressBarTextField: XCUIElement! + override class func setUp() { + UITests.setAutocompleteToggleBeforeTestcaseRuns(false) // We don't want changes in the address bar that we don't create + } + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["UITEST_MODE"] = "1" + urlStringForAddressBar = "https://duckduckgo.com/duckduckgo-help-pages/results/translation/" + urlForAddressBar = URL(string: urlStringForAddressBar) + addressBarTextField = app.windows.textFields["AddressBarViewController.addressBarTextField"] + app.launch() + app.typeKey("w", modifierFlags: [.command, .option, .shift]) // Let's enforce a single window + app.typeKey("n", modifierFlags: .command) + addressBarTextField.typeURL(urlForAddressBar, pressingEnter: false) + } + + func test_addressBar_url_canBeSelected() throws { + addressBarTextField.typeKey("a", modifierFlags: .command) // This is the behavior under test, but we will have to verify it indirectly + + addressBarTextField.typeKey("c", modifierFlags: .command) // Having selected it all, let's copy it + addressBarTextField.typeKey(.delete, modifierFlags: []) // And delete it + addressBarTextField.typeKey(.leftArrow, modifierFlags: .command) // Now let's go to the beginning of the address bar, which should be empty + addressBarTextField.typeKey("v", modifierFlags: .command) // And paste it + let addressFieldContentsWithoutTagline = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual( + urlStringForAddressBar, + addressFieldContentsWithoutTagline, + "If the contents of the address bar after typing the original url, selecting it, copying it, deleting all, and pasting it, aren't the same as the original contents we typed in, the URL was not successfully selected." + ) + } + + func test_addressBar_end_canBeNavigatedToWithCommandRightArrow() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // This is the behavior under test, but we will have to verify it indirectly + + let charactersToTrimFromSuffix = 13 + for _ in 1 ... charactersToTrimFromSuffix { + addressBarTextField.typeKey(.leftArrow, modifierFlags: [.shift]) + } + app.typeKey(.delete, modifierFlags: .command) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + let urlMinusDeletedCharacters: String = try XCTUnwrap(String(urlStringForAddressBar.dropLast(charactersToTrimFromSuffix))) + + XCTAssertEqual( + urlMinusDeletedCharacters, + addressFieldContentsAfterDelete, + "If the contents of the address bar minus the last \(charactersToTrimFromSuffix) characters from selecting backwards and deleting doesn't match the original URL string minus its last \(charactersToTrimFromSuffix), we were not at the end of the address bar string after typing command-right-arrow" + ) + } + + func test_addressBar_beginning_canBeNavigatedToWithCommandLeftArrow() throws { + addressBarTextField.typeKey(.leftArrow, modifierFlags: .command) // This is the behavior under test, but we will have to verify it indirectly + + let charactersToTrimFromPrefix = 5 + for _ in 1 ... charactersToTrimFromPrefix { + addressBarTextField.typeKey(.rightArrow, modifierFlags: [.shift]) + } + + app.typeKey(.delete, modifierFlags: .command) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + let urlMinusDeletedCharacters: String = try XCTUnwrap(String(urlStringForAddressBar.dropFirst(charactersToTrimFromPrefix))) + + XCTAssertEqual( + urlMinusDeletedCharacters, + addressFieldContentsAfterDelete, + "If the contents of the address bar minus the first \(charactersToTrimFromPrefix) characters from selecting forwards and deleting doesn't match the original URL string minus its first \(charactersToTrimFromPrefix), we were not at the start of the address bar string after typing command-left-arrow" + ) + } + + /// An important note about this test: option-arrow does not navigate through URL components, but through certain word boundary characters such as + /// "/", and possibly including others such as "-", and ".", meaning that it often navigates through URL components as a side-effect, but not + /// always. The list of word boundary characters isn't documented. This test tests whether option-arrow moves the caret between two backslashes + /// where no other word boundary characters are present: it doesn't try to test other word boundaries, and it isn't a test which demonstrates + /// movement between URL components, since results would be different if there were a word boundary character inside of the components it targets. + /// This is also true of the option-right-arrow test. + func test_addressBar_caret_canNavigateThroughWordBoundariesUsingOptionLeftArrow() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back twice using option-left-arrow + + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // This is the behavior under test, but we will have to verify it indirectly + addressBarTextField.typeKey(.rightArrow, modifierFlags: [.command, .shift]) // Select all text to the right of the caret + addressBarTextField.typeKey(.delete, modifierFlags: []) // Delete it + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + let urlMinusDeletedCharacters: String = try XCTUnwrap(String(urlStringForAddressBar.replacingOccurrences( + of: "results/translation/", + // Delete the last two components we expected to be deleted in our address bar navigation process from the original reference string for + // comparison + with: "" + ))) + + XCTAssertEqual( + urlMinusDeletedCharacters, + addressFieldContentsAfterDelete, + "If the address field contents after we have navigated using option-left-arrow do not match our expectation string, that means that at the time we selected the remainder of the address bar string and deleted it, option-left-arrow had not navigated the caret to the insertion point it should have." + ) + } + + /// Note: please read `test_addressBar_caret_canNavigateThroughWordBoundariesUsingOptionLeftArrow()` + /// for more information about what this test tests, and does not test. + func test_addressBar_caret_canNavigateThroughWordBoundariesUsingOptionRightArrow() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back twice using option-left-arrow. + addressBarTextField + .typeKey(.leftArrow, + modifierFlags: .option) // Arranging so we can test option-right-arrow in an area with no other word boundary characters. + + addressBarTextField.typeKey(.rightArrow, modifierFlags: .option) // This is the behavior under test, but we will have to verify it indirectly + addressBarTextField.typeKey(.leftArrow, modifierFlags: [.option, .shift]) // Select the component to the left of the caret + addressBarTextField.typeKey(.delete, modifierFlags: []) // Delete it + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + let urlMinusDeletedCharacters: String = try XCTUnwrap(String(urlStringForAddressBar.replacingOccurrences( + of: "results", + // Delete the last component we expected to be deleted in our address bar navigation process from the original reference string for + // comparison + with: "" + ))) + + XCTAssertEqual( + urlMinusDeletedCharacters, + addressFieldContentsAfterDelete, + "If the address field contents after we have navigated using option-right-arrow and deleted part of the URL to the left of the caret insertion point do not match our expectation string, that means that at the time we selected the part of the address bar string next to the insertion point and deleted it, option-right-arrow had not navigated the caret to the insertion point it should have." + ) + } + + func test_addressBar_url_word_canBeSelectedByDoubleClick() throws { + addressBarTextField + .typeKey(.leftArrow, + modifierFlags: .command) // let's go to the beginning of the address bar, so our coordinate will be slightly reliable despite + // window width. Even though this means we are selecting the scheme prefix as our "word", it is a good example of a text run within word + // boundaries, and this is the only reliable position for a word double-click to select because the end of the address bar has the Visit + // tagline, and the middle could contain anything. + let addressBarTextFieldCoordinate = addressBarTextField.coordinate(withNormalizedOffset: CGVector(dx: 0.01, dy: 0.5)) + addressBarTextFieldCoordinate.doubleClick() // this is the behavior under test, but we will have to evaluate it indirectly. + addressBarTextField.typeKey("c", modifierFlags: .command) // Copy the selected word + addressBarTextField.typeKey("a", modifierFlags: .command) // Select all + addressBarTextField.typeKey(.delete, modifierFlags: []) // Delete all + addressBarTextField.typeKey("v", modifierFlags: .command) // Paste the selected word + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual( + addressFieldContentsAfterDelete, + "https", + "When we double-click at the beginning of the URL, copy the selected text, delete the entire contents of the address bar, and then paste, if it doesn't equal https, that means we didn't successfully double-click a text run within word boundaries at the beginning of the address bar." + ) + } + + func test_addressBar_nearestLeftHandCharacterShouldBeDeletedByFnDelete() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back twice using option-left-arrow. + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) + + addressBarTextField.typeKey(.delete, modifierFlags: .function) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("https://duckduckgo.com/duckduckgo-help-pagesresults/translation/", addressFieldContentsAfterDelete) + } + + func test_addressBar_nearestLeftHandWordWithinBoundariesShouldBeDeletedByOptDelete() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back once using option-left-arrow. + + addressBarTextField.typeKey(.delete, modifierFlags: .option) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("https://duckduckgo.com/duckduckgo-help-pages/translation/", addressFieldContentsAfterDelete) + } + + func test_addressBar_allTextToTheLeftShouldBeDeletedByCommandDelete() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back once using option-left-arrow. + + addressBarTextField.typeKey(.delete, modifierFlags: .command) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("translation/", addressFieldContentsAfterDelete) + } + + func test_addressBar_nearestRightHandCharacterShouldBeDeletedByFnForwardDelete() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back twice using option-left-arrow. + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) + + addressBarTextField.typeKey(.forwardDelete, modifierFlags: .function) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("https://duckduckgo.com/duckduckgo-help-pages/esults/translation/", addressFieldContentsAfterDelete) + } + + func test_addressBar_nearestRightHandWordWithinBoundariesShouldBeDeletedByOptForwardDelete() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back once using option-left-arrow. + + addressBarTextField + .typeKey(.forwardDelete, modifierFlags: .option) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("https://duckduckgo.com/duckduckgo-help-pages/results//", addressFieldContentsAfterDelete) + } + + func test_addressBar_allTextToTheRightShouldBeDeletedByCommandForwardDelete() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back once using option-left-arrow. + + addressBarTextField + .typeKey(.forwardDelete, modifierFlags: .command) + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("https://duckduckgo.com/duckduckgo-help-pages/results/", addressFieldContentsAfterDelete) + } + + func test_addressBar_commandZShouldUndoLastAction() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back once using option-left-arrow. + addressBarTextField + .typeKey(.forwardDelete, modifierFlags: .command) // Delete word + + addressBarTextField.typeKey("z", modifierFlags: .command) // Undo deletion + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual(urlStringForAddressBar, addressFieldContentsAfterDelete) + } + + func test_addressBar_shiftCommandZShouldRedoLastUndoneAction() throws { + addressBarTextField.typeKey(.rightArrow, modifierFlags: .command) // move caret to end + addressBarTextField.typeKey(.leftArrow, modifierFlags: .option) // Step back once using option-left-arrow. + addressBarTextField + .typeKey(.forwardDelete, modifierFlags: .command) // Delete word + addressBarTextField.typeKey("z", modifierFlags: .command) // Undo deletion + + addressBarTextField.typeKey("z", modifierFlags: [.shift, .command]) // Redo deletion + let addressFieldContentsAfterDelete = try XCTUnwrap(addressBarTextField.value as? String).removingTagLine() + + XCTAssertEqual("https://duckduckgo.com/duckduckgo-help-pages/results/", addressFieldContentsAfterDelete) + } +} + +private extension String { + func removingTagLine() -> String { + return self.components(separatedBy: " ").first ?? self // If there is no space in the URL, the tagline isn't attached + } +} diff --git a/UITests/AutocompleteTests.swift b/UITests/AutocompleteTests.swift index 4c730f0f80..cea246ca33 100644 --- a/UITests/AutocompleteTests.swift +++ b/UITests/AutocompleteTests.swift @@ -31,6 +31,10 @@ class AutocompleteTests: XCTestCase { private var siteTitleForBookmarkedSite: String! private var siteTitleForHistorySite: String! + override class func setUp() { + UITests.setAutocompleteToggleBeforeTestcaseRuns(true) // These tests require autocomplete to be on + } + override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() diff --git a/UITests/Common/UITests.swift b/UITests/Common/UITests.swift index 438cf5be4d..4bd7581e32 100644 --- a/UITests/Common/UITests.swift +++ b/UITests/Common/UITests.swift @@ -49,4 +49,43 @@ enum UITests { static func randomPageTitle(length: Int) -> String { return String(UUID().uuidString.prefix(length)) } + + /// This is intended for setting an autocomplete checkbox state that extends across all test cases and is only run once in the class override + /// setup() of the case. Setting the autocomplete checkbox state for an individual test shouldn't start and terminate the app, as this function + /// does. + /// - Parameter requestedToggleState: How the autocomplete checkbox state should be set + static func setAutocompleteToggleBeforeTestcaseRuns(_ requestedToggleState: Bool) { + let app = XCUIApplication() + app.launch() + + app.typeKey(",", modifierFlags: [.command]) // Open settings + let generalPreferencesButton = app.buttons["PreferencesSidebar.generalButton"] + let autocompleteToggle = app.checkBoxes["PreferencesGeneralView.showAutocompleteSuggestions"] + XCTAssertTrue( + generalPreferencesButton.waitForExistence(timeout: UITests.Timeouts.elementExistence), + "The user settings appearance section button didn't become available in a reasonable timeframe." + ) + generalPreferencesButton.click(forDuration: 0.5, thenDragTo: generalPreferencesButton) + + let currentToggleState = try? XCTUnwrap( + autocompleteToggle.value as? Bool, + "It wasn't possible to get the \"Autocomplete\" value as a Bool" + ) + + switch (requestedToggleState, currentToggleState) { // Click autocomplete toggle if it is different than our request + case (false, true), (true, false): + autocompleteToggle.click() + default: + break + } + app.terminate() + } + + /// A debug function that is going to need some other functionality in order to be useful for debugging address bar focus issues + static func openVanillaBrowser() { + let app = XCUIApplication() + let openVanillaBrowser = app.menuItems["MainMenu.openVanillaBrowser"] + openVanillaBrowser.clickAfterExistenceTestSucceeds() + app.typeKey("w", modifierFlags: [.command, .option]) + } } From 670d54c3ddfb0d530dd1d67404b042fbd04b0dc4 Mon Sep 17 00:00:00 2001 From: kshann Date: Fri, 12 Apr 2024 22:18:14 +1000 Subject: [PATCH 079/221] UI Test workflows MacOS 13 + 14 (#2614) Task/Issue URL: https://app.asana.com/0/0/1205717523425891/f Tech Design URL: CC: @ayoy **Description**: 13/14 flows from this PR https://github.com/duckduckgo/macos-browser/pull/2515 Have left asana notifications commented out for now. **Steps to test this PR**: 1. Example run https://github.com/duckduckgo/macos-browser/actions/runs/8660861087 2. If you want to run `gh workflow run "ui_tests.yml" --ref "kieran/ui-tests-macos1314"` --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .github/workflows/ui_tests.yml | 103 +++++++++++++++++++++++ Configuration/Tests/TestsServer.xcconfig | 2 + 2 files changed, 105 insertions(+) create mode 100644 .github/workflows/ui_tests.yml diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml new file mode 100644 index 0000000000..44bbfa8410 --- /dev/null +++ b/.github/workflows/ui_tests.yml @@ -0,0 +1,103 @@ +name: UI Tests + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * 1-5' # 3AM UTC offsetted to legacy to avoid action-junit-report@v4 bug + +jobs: + ui-tests: + name: UI tests + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + runner: [macos-13-xlarge, macos-14-xlarge] + + timeout-minutes: 120 + + steps: + - name: Check out the code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set cache key hash + run: | + has_only_tags=$(jq '[ .pins[].state | has("version") ] | all' DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved) + if [[ "$has_only_tags" == "true" ]]; then + echo "cache_key_hash=${{ hashFiles('DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}" >> $GITHUB_ENV + else + echo "Package.resolved contains dependencies specified by branch or commit, skipping cache." + fi + + - name: Cache SPM + if: env.cache_key_hash + uses: actions/cache@v3 + with: + path: DerivedData/SourcePackages + key: ${{ runner.os }}-spm-${{ env.cache_key_hash }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Install Apple Developer ID Application certificate + uses: ./.github/actions/install-certs-and-profiles + with: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }} + RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }} + DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }} + DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }} + NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2 }} + NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2 }} + NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2 }} + NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2 }} + NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }} + NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }} + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_$(<.xcode-version).app/Contents/Developer + + - name: Build and run UI Testing + run: | + defaults write com.duckduckgo.macos.browser.review moveToApplicationsFolderAlertSuppress 1 + defaults write com.duckduckgo.macos.browser.review onboarding.finished -bool true + set -o pipefail && xcodebuild test \ + -scheme "UI Tests" \ + -configuration Review \ + -derivedDataPath DerivedData \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + -test-iterations 2 \ + -retry-tests-on-failure \ + | tee xcodebuild.log \ + | xcbeautify --report junit --report-path . --junit-report-filename ui-tests.xml + + # - name: Create Asana task when workflow failed + # if: ${{ failure() }} && github.ref == 'refs/heads/main' + # run: | + # curl -s "https://app.asana.com/api/1.0/tasks" \ + # --header "Accept: application/json" \ + # --header "Authorization: Bearer ${{ secrets.ASANA_ACCESS_TOKEN }}" \ + # --header "Content-Type: application/json" \ + # --data ' { "data": { "name": "GH Workflow Failure - UI Tests", "projects": [ "${{ vars.MACOS_APP_DEVELOPMENT_ASANA_PROJECT_ID }}" ], "notes" : "The end to end workflow has failed. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }' + + - name: Publish tests report + uses: mikepenz/action-junit-report@v4 + if: always() + with: + check_name: "Test Report ${{ matrix.runner }}" + report_paths: ui-tests.xml + + - name: Upload logs when workflow failed + uses: actions/upload-artifact@v4 + if: failure() + with: + name: "BuildLogs ${{ matrix.runner }}" + path: | + xcodebuild.log + DerivedData/Logs/Test/*.xcresult + ~/Library/Logs/DiagnosticReports/* + retention-days: 7 \ No newline at end of file diff --git a/Configuration/Tests/TestsServer.xcconfig b/Configuration/Tests/TestsServer.xcconfig index b401906d9b..e8bc6f5e79 100644 --- a/Configuration/Tests/TestsServer.xcconfig +++ b/Configuration/Tests/TestsServer.xcconfig @@ -18,3 +18,5 @@ #include "../Common.xcconfig" PRODUCT_NAME = $(TARGET_NAME); +CODE_SIGNING_ALLOWED[config=Review][sdk=macosx*] = NO +CODE_SIGNING_ALLOWED[config=CI][sdk=macosx*] = NO \ No newline at end of file From fcf8390090ba8c4f7917380690a9c3fffe97bc7e Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:59:33 +0200 Subject: [PATCH 080/221] Sabrina/ssl errors (#2511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206880509171835/f Tech Design URL: https://app.asana.com/0/0/1206862686877685/f CC: **Description**: In case of SSL error will give the user the option of bypass the error and visit the site anyway. See [Figma →](https://www.figma.com/file/MyutO3AFHG209lVBMvP2Z3/Certificate-Bypass-3?type=design&node-id=38-7142&mode=design&t=AhVdIhANKLgNZOBp-11) and https://app.asana.com/0/72649045549333/1206755044584061 --- DuckDuckGo.xcodeproj/project.pbxproj | 46 +- .../xcshareddata/swiftpm/Package.resolved | 14 +- .../DuckDuckGo Privacy Browser.xcscheme | 1 + .../Contents.json | 12 + .../Red-Alert-Circle-16.svg | 17 + DuckDuckGo/Common/Localizables/UserText.swift | 36 + .../Utilities/CertificateTrustEvaluator.swift | 32 + .../ErrorPage/ErrorPageHTMLTemplate.swift | 128 + .../ErrorPage/SSLErrorPageUserScript.swift | 77 + .../FeatureFlagging/Model/FeatureFlag.swift | 3 + DuckDuckGo/InfoPlist.xcstrings | 2 +- DuckDuckGo/Localizable.xcstrings | 945 ++- .../AddressBarButtonsViewController.swift | 3 +- DuckDuckGo/Tab/Model/Tab+Navigation.swift | 3 + DuckDuckGo/Tab/Model/Tab.swift | 28 +- .../SSLErrorPageTabExtension.swift | 180 + .../Tab/TabExtensions/TabExtensions.swift | 5 + DuckDuckGo/Tab/UserScripts/UserScripts.swift | 16 +- DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 18 +- IntegrationTests/Tab/AddressBarTests.swift | 92 + .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../Screenshot 2024-03-14 alle 13.19.29.png | Bin 0 -> 85134 bytes LocalPackages/SubscriptionUI/Package.swift | 2 +- .../SyncUI/Resources/Localizable.xcstrings | 288 + .../ErrorPageTabExtensionTest.swift | 411 ++ .../SSLErrorPageUserScriptTests.swift | 122 + scripts/assets/loc/en.xliff | 5106 +++++++++++++++++ 28 files changed, 7501 insertions(+), 90 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Red-Alert-Circle-16.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Red-Alert-Circle-16.imageset/Red-Alert-Circle-16.svg create mode 100644 DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift create mode 100644 DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift create mode 100644 DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift create mode 100644 LocalPackages/Screenshot 2024-03-14 alle 13.19.29.png create mode 100644 UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift create mode 100644 UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift create mode 100644 scripts/assets/loc/en.xliff diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bafb4c7c68..55941a8752 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2197,6 +2197,19 @@ 569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */; }; 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; + 56BA1E752BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; + 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; + 56BA1E772BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; + 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; + 56BA1E802BAB2E43001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; + 56BA1E822BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; + 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; + 56BA1E842BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; + 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; + 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; + 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; + 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; + 56BA1E8C2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56CEE9102B7A72FE00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; @@ -4067,6 +4080,11 @@ 569277C029DDCBB500B633EF /* HomePageContinueSetUpModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageContinueSetUpModel.swift; sourceTree = ""; }; 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueSetUpModelTests.swift; sourceTree = ""; }; 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarUrlExtensionsTests.swift; sourceTree = ""; }; + 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageTabExtension.swift; sourceTree = ""; }; + 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageTabExtensionTest.swift; sourceTree = ""; }; + 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScript.swift; sourceTree = ""; }; + 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScriptTests.swift; sourceTree = ""; }; + 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateTrustEvaluator.swift; sourceTree = ""; }; 56CEE9092B7A66C500CF10AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDataImportProvider.swift; sourceTree = ""; }; @@ -6357,6 +6375,7 @@ 4BB88B4F25B7BA2B006F6B06 /* TabInstrumentation.swift */, 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */, 4B9579202AC687170062CA31 /* HardwareModel.swift */, + 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */, ); path = Utilities; sourceTree = ""; @@ -6564,6 +6583,14 @@ path = Mocks; sourceTree = ""; }; + 56BA1E852BAC820D001CF69F /* UserScripts */ = { + isa = PBXGroup; + children = ( + 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */, + ); + path = UserScripts; + sourceTree = ""; + }; 7B1E819A27C8874900FF0E60 /* Autofill */ = { isa = PBXGroup; children = ( @@ -7318,6 +7345,7 @@ AA585D93248FD31400E9A3E2 /* UnitTests */ = { isa = PBXGroup; children = ( + 56BA1E852BAC820D001CF69F /* UserScripts */, C13909F22B85FD60001626ED /* Autofill */, 5629846D2AC460DF00AC20EB /* Sync */, B6A5A28C25B962CB00AA7ADA /* App */, @@ -8411,6 +8439,7 @@ B66260E529ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift */, B66260DC29AC5D4300E9E3EE /* NavigationProtectionTabExtension.swift */, B626A76C29928B1600053070 /* TestsClosureNavigationResponder.swift */, + 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */, ); path = TabExtensions; sourceTree = ""; @@ -8488,6 +8517,7 @@ isa = PBXGroup; children = ( B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */, + 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */, ); path = ErrorPage; sourceTree = ""; @@ -8667,6 +8697,7 @@ B626A7632992506A00053070 /* SerpHeadersNavigationResponderTests.swift */, 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */, 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */, + 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */, ); path = TabExtensionsTests; sourceTree = ""; @@ -10377,6 +10408,7 @@ 3706FAC2293F65D500E42796 /* FaviconSelector.swift in Sources */, B696AFFC2AC5924800C93203 /* FileLineError.swift in Sources */, F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, + 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */, B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */, 3706FAC4293F65D500E42796 /* PrintingUserScript.swift in Sources */, 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, @@ -10529,6 +10561,7 @@ 3706FB35293F65D500E42796 /* FlatButton.swift in Sources */, 3706FB36293F65D500E42796 /* PinnedTabView.swift in Sources */, 3706FB37293F65D500E42796 /* DataEncryption.swift in Sources */, + 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, 37197EA82942443D00394917 /* BrowserTabViewController.swift in Sources */, 3706FB39293F65D500E42796 /* PrivacyDashboardPopover.swift in Sources */, @@ -10896,6 +10929,7 @@ B6685E4029A606190043D2EE /* WorkspaceProtocol.swift in Sources */, 3706FC2F293F65D500E42796 /* MouseOverButton.swift in Sources */, 3706FC30293F65D500E42796 /* FireInfoViewController.swift in Sources */, + 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 3706FC31293F65D500E42796 /* PermissionButton.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, @@ -11118,6 +11152,7 @@ 3706FE04293F661700E42796 /* TreeControllerTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, + 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, 3706FE07293F661700E42796 /* PasswordManagementItemListModelTests.swift in Sources */, 3706FE08293F661700E42796 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 3706FE09293F661700E42796 /* VariantManagerTests.swift in Sources */, @@ -11295,6 +11330,7 @@ 3706FE79293F661700E42796 /* AppearancePreferencesTests.swift in Sources */, 3706FE7A293F661700E42796 /* FirePopoverViewModelTests.swift in Sources */, 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, + 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, @@ -11796,6 +11832,7 @@ 4B957A272AC7AE700062CA31 /* PrivacyDashboardPopover.swift in Sources */, 4B957A282AC7AE700062CA31 /* TestsClosureNavigationResponder.swift in Sources */, 4B957A292AC7AE700062CA31 /* RootView.swift in Sources */, + 56BA1E772BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 4B37EE7C2B4CFF8000A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */, 4B957A2A2AC7AE700062CA31 /* AddressBarTextField.swift in Sources */, 4B957A2B2AC7AE700062CA31 /* FocusRingView.swift in Sources */, @@ -11940,9 +11977,11 @@ 4B520F652BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 4B957AA02AC7AE700062CA31 /* BookmarkManager.swift in Sources */, 4B957AA12AC7AE700062CA31 /* AboutModel.swift in Sources */, + 56BA1E8C2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */, 4B957AA22AC7AE700062CA31 /* PasswordManagementCreditCardItemView.swift in Sources */, 3158B1552B0BF75900AF130C /* LoginItem+DataBrokerProtection.swift in Sources */, 4B957AA32AC7AE700062CA31 /* NSTextFieldExtension.swift in Sources */, + 56BA1E842BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 4B957AA42AC7AE700062CA31 /* BWManagement.swift in Sources */, 4B957AA52AC7AE700062CA31 /* FireproofDomainsContainer.swift in Sources */, 4B957AA62AC7AE700062CA31 /* ExternalAppSchemeHandler.swift in Sources */, @@ -12537,6 +12576,7 @@ 4BE65485271FCD7B008D1D63 /* LoginFaviconView.swift in Sources */, 4B0511CA262CAA5A00F6079C /* FireproofDomainsViewController.swift in Sources */, AA4D700725545EF800C3411E /* URLEventHandler.swift in Sources */, + 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */, 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */, AA92127725ADA07900600CD4 /* WKWebViewExtension.swift in Sources */, AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */, @@ -12796,6 +12836,7 @@ 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, + 56BA1E752BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 85589E8227BBB8630038AD11 /* HomePageView.swift in Sources */, B6BF5D932947199A006742B1 /* SerpHeadersNavigationResponder.swift in Sources */, 569277C129DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */, @@ -12870,6 +12911,7 @@ B66260E629ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, EA0BA3A9272217E6002A0B6C /* ClickToLoadUserScript.swift in Sources */, AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */, + 56BA1E822BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */, B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */, @@ -13154,6 +13196,7 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, + 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */, 4B9292BC2667103100AD2C21 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 4B9DB05A2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, @@ -13300,6 +13343,7 @@ B6AA64732994B43300D99CD6 /* FutureExtensionTests.swift in Sources */, B603975329C1FFAE00902A34 /* ExpectedNavigationExtension.swift in Sources */, EA1E52B52798CF98002EC53C /* ClickToLoadModelTests.swift in Sources */, + 56BA1E802BAB2E43001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, B603975029C1FF5F00902A34 /* TestsURLExtension.swift in Sources */, B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */, 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, @@ -14500,7 +14544,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 134.0.0; + version = 134.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 403ec8030a..48d2d9aea8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "bc70d1a27263cc97a4060ac9e73ec10929c28a29", - "version" : "134.0.0" + "revision" : "b0749d25996c0fa18be07b7851f02ebb3b9fab50", + "version" : "134.0.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "62d5dc3d02f6a8347dc5f0b52162a0107d38b74c", - "version" : "5.8.0" + "revision" : "1bb3bc5eb565735051f342a87b5405d4374876c7", + "version" : "5.12.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "620921fea14569eb00745cb5a44890d5890d99ec", - "version" : "3.4.0" + "revision" : "14b13d0c3db38f471ce4ba1ecb502ee1986c84d7", + "version" : "3.5.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index aefd4b6b53..b70be830d9 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -69,6 +69,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" + language = "en" codeCoverageEnabled = "YES"> + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 36e9c3d90d..0337f4f9a6 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -215,10 +215,46 @@ struct UserText { static let tabPreferencesTitle = NSLocalizedString("tab.preferences.title", value: "Settings", comment: "Tab preferences title") static let tabBookmarksTitle = NSLocalizedString("tab.bookmarks.title", value: "Bookmarks", comment: "Tab bookmarks title") static let tabOnboardingTitle = NSLocalizedString("tab.onboarding.title", value: "Welcome", comment: "Tab onboarding title") + + // MARK: Error Pages static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Failed to open page", comment: "Tab error title") static let errorPageHeader = NSLocalizedString("page.error.header", value: "DuckDuckGo can’t load this page.", comment: "Error page heading text") static let webProcessCrashPageHeader = NSLocalizedString("page.crash.header", value: "This webpage has crashed.", comment: "Error page heading text shown when a Web Page process had crashed") static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed") + static let sslErrorPageHeader = NSLocalizedString("ssl.error.page.header", value: "Warning: This site may be insecure", comment: "Title shown in an error page that warn users of security risks on a website due to SSL issues") + static let sslErrorPageTabTitle = NSLocalizedString("ssl.error.page.tab.title", value: "Warning: Site May Be Insecure", comment: "Title shown in an error page tab that warn users of security risks on a website due to SSL issues") + static func sslErrorPageBody(_ domain: String) -> String { + let localized = NSLocalizedString("ssl.error.page.body", + value: "The certificate for this site is invalid. You might be connecting to a server that is pretending to be %1$@ which could put your confidential information at risk.", + comment: "Error description shown in an error page that warns users of security risks on a website due to SSL issues. %1$@ represent the site domain.") + return String(format: localized, domain) + } + static let sslErrorPageAdvancedButton = NSLocalizedString("ssl.error.page.advanced.button", value: "Advanced…", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to see advanced options on click.") + static let sslErrorPageLeaveSiteButton = NSLocalizedString("ssl.error.page.leave.site.button", value: "Leave This Site", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to leave the website and navigate to previous page.") + static let sslErrorPageVisitSiteButton = NSLocalizedString("ssl.error.page.visit.site.button", value: "Accept Risk and Visit Site", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to visit the website anyway despite the risks.") + static let sslErrorAdvancedInfoTitle = NSLocalizedString("ssl.error.page.advanced.info.title", value: "DuckDuckGo warns you when a website has an invalid certificate.", comment: "Title of the Advanced info section shown in an error page that warns users of security risks on a website due to SSL issues.") + static let sslErrorAdvancedInfoBodyWrongHost = NSLocalizedString("ssl.error.page.advanced.info.body.wrong.host", value: "It’s possible that the website is misconfigured or that an attacker has compromised your connection.", comment: "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.") + static let sslErrorAdvancedInfoBodyExpired = NSLocalizedString("ssl.error.page.advanced.info.body.expired", value: "It’s possible that the website is misconfigured, that an attacker has compromised your connection, or that your system clock is incorrect.", comment: "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.") + static func sslErrorCertificateExpiredMessage(_ domain: String) -> String { + let localized = NSLocalizedString("ssl.error.certificate.expired.message", + value: "The security certificate for %1$@ is expired.", + comment: "Describes an SSL error where a website's security certificate is expired. '%1$@' is a placeholder for the website's domain.") + return String(format: localized, domain) + } + static func sslErrorCertificateWrongHostMessage(_ domain: String, eTldPlus1: String) -> String { + let localized = NSLocalizedString("ssl.error.wrong.host.message", + value: "The security certificate for %1$@ does not match *.%2$@.", + comment: "Explains an SSL error when a site's certificate doesn't match its domain. '%1$@' is the site's domain.") + return String(format: localized, domain, eTldPlus1) + } + static func sslErrorCertificateSelfSignedMessage(_ domain: String) -> String { + let localized = NSLocalizedString("ssl.error.self.signed.message", + value: "The security certificate for %1$@ is not trusted by your device's operating system.", + comment: "Warns the user that the site's security certificate is self-signed and not trusted. '%1$@' is the site's domain.") + return String(format: localized, domain) + } + + static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "This string represents a prompt or button label prompting the user to open system settings") diff --git a/DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift b/DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift new file mode 100644 index 0000000000..541f61c95d --- /dev/null +++ b/DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift @@ -0,0 +1,32 @@ +// +// CertificateTrustEvaluator.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 + +protocol CertificateTrustEvaluating { + func evaluateCertificateTrust(trust: SecTrust?) -> Bool? +} + +struct CertificateTrustEvaluator: CertificateTrustEvaluating { + func evaluateCertificateTrust(trust: SecTrust?) -> Bool? { + var error: CFError? + guard let trust else { return nil } + let result = SecTrustEvaluateWithError(trust, &error) + return result + } +} diff --git a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift index af9d04103e..a288139546 100644 --- a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift +++ b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift @@ -19,6 +19,7 @@ import Foundation import ContentScopeScripts import WebKit +import Common struct ErrorPageHTMLTemplate { @@ -43,3 +44,130 @@ struct ErrorPageHTMLTemplate { } } + +struct SSLErrorPageHTMLTemplate { + let domain: String + let errorCode: Int + let tld = TLD() + + static var htmlTemplatePath: String { + guard let file = ContentScopeScripts.Bundle.path(forResource: "index", ofType: "html", inDirectory: "pages/sslerrorpage") else { + assertionFailure("HTML template not found") + return "" + } + return file + } + + func makeHTMLFromTemplate() -> String { + let sslError = SSLErrorType.forErrorCode(errorCode) + guard let html = try? String(contentsOfFile: Self.htmlTemplatePath) else { + assertionFailure("Should be able to load template") + return "" + } + let eTldPlus1 = tld.eTLDplus1(domain) ?? domain + let loadTimeData = createJSONString(header: sslError.header, body: sslError.body(for: domain), advancedButton: sslError.advancedButton, leaveSiteButton: sslError.leaveSiteButton, advancedInfoHeader: sslError.advancedInfoTitle, specificMessage: sslError.specificMessage(for: domain, eTldPlus1: eTldPlus1), advancedInfoBody: sslError.advancedInfoBody, visitSiteButton: sslError.visitSiteButton) + return html.replacingOccurrences(of: "$LOAD_TIME_DATA$", with: loadTimeData, options: .literal) + } + + private func createJSONString(header: String, body: String, advancedButton: String, leaveSiteButton: String, advancedInfoHeader: String, specificMessage: String, advancedInfoBody: String, visitSiteButton: String) -> String { + let innerDictionary: [String: Any] = [ + "header": header.escapedUnicodeHtmlString(), + "body": body.escapedUnicodeHtmlString(), + "advancedButton": advancedButton.escapedUnicodeHtmlString(), + "leaveSiteButton": leaveSiteButton.escapedUnicodeHtmlString(), + "advancedInfoHeader": advancedInfoHeader.escapedUnicodeHtmlString(), + "specificMessage": specificMessage.escapedUnicodeHtmlString(), + "advancedInfoBody": advancedInfoBody.escapedUnicodeHtmlString(), + "visitSiteButton": visitSiteButton.escapedUnicodeHtmlString() + ] + + let outerDictionary: [String: Any] = [ + "strings": innerDictionary + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: outerDictionary, options: .prettyPrinted) + if let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } else { + return "Error: Could not encode jsonData to String." + } + } catch { + return "Error: \(error.localizedDescription)" + } + } + +} + +public enum SSLErrorType { + case expired + case wrongHost + case selfSigned + case invalid + + var header: String { + return UserText.sslErrorPageHeader + } + + func body(for domain: String) -> String { + let boldDomain = "\(domain)" + return UserText.sslErrorPageBody(boldDomain) + } + + var advancedButton: String { + return UserText.sslErrorPageAdvancedButton + } + + var leaveSiteButton: String { + return UserText.sslErrorPageLeaveSiteButton + } + + var visitSiteButton: String { + return UserText.sslErrorPageVisitSiteButton + } + + var advancedInfoTitle: String { + return UserText.sslErrorAdvancedInfoTitle + } + + var advancedInfoBody: String { + switch self { + case .expired: + return UserText.sslErrorAdvancedInfoBodyExpired + case .wrongHost: + return UserText.sslErrorAdvancedInfoBodyWrongHost + case .selfSigned: + return UserText.sslErrorAdvancedInfoBodyWrongHost + case .invalid: + return UserText.sslErrorAdvancedInfoBodyWrongHost + } + } + + func specificMessage(for domain: String, eTldPlus1: String) -> String { + let boldDomain = "\(domain)" + let boldETldPlus1 = "\(eTldPlus1)" + switch self { + case .expired: + return UserText.sslErrorCertificateExpiredMessage(boldDomain) + case .wrongHost: + return UserText.sslErrorCertificateWrongHostMessage(boldDomain, eTldPlus1: boldETldPlus1) + case .selfSigned: + return UserText.sslErrorCertificateSelfSignedMessage(boldDomain) + case .invalid: + return UserText.sslErrorCertificateSelfSignedMessage(boldDomain) + } + } + + static func forErrorCode(_ errorCode: Int) -> Self { + switch Int32(errorCode) { + case errSSLCertExpired: + return .expired + case errSSLHostNameMismatch: + return .wrongHost + case errSSLXCertChainInvalid: + return .selfSigned + default: + return .invalid + } + } +} diff --git a/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift b/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift new file mode 100644 index 0000000000..1f3487834e --- /dev/null +++ b/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift @@ -0,0 +1,77 @@ +// +// SSLErrorPageUserScript.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 UserScript + +final class SSLErrorPageUserScript: NSObject, Subfeature { + enum MessageName: String, CaseIterable { + case leaveSite + case visitSite + } + + public let messageOriginPolicy: MessageOriginPolicy = .all + public let featureName: String = "sslErrorPage" + + var isEnabled: Bool = false + var failingURL: URL? + + weak var broker: UserScriptMessageBroker? + weak var delegate: SSLErrorPageUserScriptDelegate? + + func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + + @MainActor + func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { + guard isEnabled else { return nil } + switch MessageName(rawValue: methodName) { + case .leaveSite: + return handleLeaveSiteAction + case .visitSite: + return handleVisitSiteAction + default: + assertionFailure("SSLErrorPageUserScript: Failed to parse User Script message: \(methodName)") + return nil + } + } + + @MainActor + func handleLeaveSiteAction(params: Any, message: UserScriptMessage) -> Encodable? { + delegate?.leaveSite() + return nil + } + + @MainActor + func handleVisitSiteAction(params: Any, message: UserScriptMessage) -> Encodable? { + delegate?.visitSite() + return nil + } + + // MARK: - UserValuesNotification + + struct UserValuesNotification: Encodable { + let userValuesNotification: UserValues + } +} + +protocol SSLErrorPageUserScriptDelegate: AnyObject { + func leaveSite() + func visitSite() +} diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 8b784addef..32c34a8b9b 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -21,6 +21,7 @@ import BrowserServicesKit public enum FeatureFlag: String { case debugMenu + case sslCertificatesBypass /// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder /// https://app.asana.com/0/1199230911884351/1205979030848528/f @@ -34,6 +35,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .appendAtbToSerpQueries: return .internalOnly + case .sslCertificatesBypass: + return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) } } } diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index e4f7972ce7..28e181ef08 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -363,4 +363,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 04f8d61b80..578c6d0b12 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -769,6 +769,7 @@ }, "Add Folder" : { "comment" : "Add Folder popover: Create folder button", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -1115,6 +1116,7 @@ }, "Address:" : { "comment" : "Add Bookmark dialog bookmark url field heading", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -7272,6 +7274,7 @@ }, "Bookmark Added" : { "comment" : "Bookmark Added popover title", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -9042,7 +9045,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lesezeichen hinzufügen" + "value" : "Add Bookmark" } }, "en" : { @@ -9054,43 +9057,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Añadir marcador" + "value" : "Add Bookmark" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ajouter un signet" + "value" : "Add Bookmark" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiungi ai segnalibri" + "value" : "Add Bookmark" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bladwijzer toevoegen" + "value" : "Add Bookmark" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dodaj zakładkę" + "value" : "Add Bookmark" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Adicionar marcador" + "value" : "Add Bookmark" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Добавить закладку" + "value" : "Add Bookmark" } } } @@ -9162,7 +9165,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lesezeichen bearbeiten" + "value" : "Edit Bookmark" } }, "en" : { @@ -9174,43 +9177,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Editar marcador" + "value" : "Edit Bookmark" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Modifier le signet" + "value" : "Edit Bookmark" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modifica segnalibro" + "value" : "Edit Bookmark" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bladwijzer bewerken" + "value" : "Edit Bookmark" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Edytuj zakładkę" + "value" : "Edit Bookmark" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Editar marcador" + "value" : "Edit Bookmark" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Редактировать закладку" + "value" : "Edit Bookmark" } } } @@ -11289,6 +11292,7 @@ }, "Copy" : { "comment" : "Command", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -12528,6 +12532,7 @@ }, "Delete" : { "comment" : "Command", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15724,6 +15729,7 @@ }, "Edit…" : { "comment" : "Command", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -30333,44 +30339,44 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Comparte tus ideas" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Partagez votre avis" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "it" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Comunicaci la tua opinione" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Deel je gedachten" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Podziel się przemyśleniami" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Partilha as tuas opiniões" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Поделиться соображениями" + "state" : "translated", + "value" : "Sign Up To Participate" } } } @@ -30393,44 +30399,44 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Responde a nuestra breve encuesta y ayúdanos a crear el mejor navegador." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Répondez à notre courte enquête et aidez-nous à créer le meilleur navigateur." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "it" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Rispondi al nostro breve sondaggio e aiutaci a creare il browser migliore." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "nl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vul onze korte enquête in en help ons de beste browser te bouwen." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "pl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Weź udział w krótkiej ankiecie i pomóż nam opracować najlepszą przeglądarkę." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "pt" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Responde ao nosso curto inquérito e ajuda-nos a criar o melhor navegador." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Пройдите короткий опрос и помогите DuckDuckGo стать лучшим из браузеров." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } } } @@ -30453,44 +30459,44 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Cuéntanos qué te ha traído hasta aquí" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Dites-nous ce qui vous amène ici" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "it" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Raccontaci cosa ti ha portato qui" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertel ons wat je hier heeft gebracht" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Powiedz nam, co Cię tu sprowadziło" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Diz-nos o que te trouxe aqui" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Что привело вас к нам" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } } } @@ -30690,6 +30696,48 @@ "state" : "new", "value" : "Sign Up To Participate" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } } } }, @@ -30697,11 +30745,59 @@ "comment" : "Summary of the card on the new tab page that invites users to partecipate to a survey", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Join an interview with a member of our research team to help us build the best browser." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } } } }, @@ -30709,6 +30805,12 @@ "comment" : "Title of the Day 14 durvey of the Set Up section in the home page", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Share Your Thoughts With Us" + } + }, "en" : { "stringUnit" : { "state" : "new", @@ -31001,7 +31103,7 @@ }, "no.access.to.selected.folder" : { "comment" : "Alert presented to user if the app doesn't have rights to access selected folder", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -31061,7 +31163,7 @@ }, "no.access.to.selected.folder.header" : { "comment" : "Header of the alert dialog informing user about failed download", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -45885,6 +45987,7 @@ }, "Remove" : { "comment" : "Remove bookmark button title", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48661,6 +48764,726 @@ } } }, + "ssl.error.certificate.expired.message" : { + "comment" : "Describes an SSL error where a website's security certificate is expired. '%1$@' is a placeholder for the website's domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Sicherheitszertifikat für %1$@ ist abgelaufen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The security certificate for %1$@ is expired." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El certificado de seguridad de %1$@ ha caducado." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de sécurité de %1$@ a expiré." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di sicurezza per %1$@ è scaduto." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het beveiligingscertificaat voor %1$@ is verlopen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat zabezpieczeń dla %1$@ wygasł." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O certificado de segurança de %1$@ expirou." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Срок действия сертификата безопасности сайта %1$@ истек." + } + } + } + }, + "ssl.error.page.advanced.button" : { + "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to see advanced options on click.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erweitert …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Advanced…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanzadas..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Options avancées" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanzate…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geavanceerd ..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaawansowane..." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avançado…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дополнительно..." + } + } + } + }, + "ssl.error.page.advanced.info.body.expired" : { + "comment" : "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es ist möglich, dass die Website falsch konfiguriert ist, ein Angreifer deine Verbindung kompromittiert hat oder deine Systemuhr falsch ist." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "It’s possible that the website is misconfigured, that an attacker has compromised your connection, or that your system clock is incorrect." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es posible que el sitio web esté mal configurado, que un atacante haya comprometido tu conexión o que el reloj del sistema no sea correcto." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il est possible que le site soit mal configuré, qu'un pirate ait compromis votre connexion ou que l'horloge de votre système soit incorrecte." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È possibile che il sito web non sia configurato correttamente, che un aggressore abbia compromesso la tua connessione o che l'orologio del tuo sistema non sia corretto." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mogelijk is de website verkeerd geconfigureerd, heeft een aanvaller je verbinding gecompromitteerd of staat de klok van je systeem niet goed." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możliwe, że witryna jest błędnie skonfigurowana, osoba atakująca naruszyła połączenie lub ustawienie zegara systemowego jest nieprawidłowe." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "É possível que o site esteja mal configurado, que um invasor tenha comprometido a tua ligação ou que o teu relógio do sistema esteja incorreto." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Возможно, неверно задана конфигурация сайта, соединение перехвачено злоумышленником либо неправильно настроены системные часы." + } + } + } + }, + "ssl.error.page.advanced.info.body.wrong.host" : { + "comment" : "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möglicherweise ist die Website falsch konfiguriert oder ein Angreifer hat deine Verbindung kompromittiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "It’s possible that the website is misconfigured or that an attacker has compromised your connection." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es posible que el sitio web esté mal configurado o que un atacante haya comprometido tu conexión." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il est possible que le site soit mal configuré ou qu'un pirate ait compromis votre connexion." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È possibile che il sito web non sia configurato correttamente o che un malintenzionato abbia compromesso la tua connessione." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mogelijk is de website verkeerd geconfigureerd of heeft een aanvaller je verbinding gecompromitteerd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możliwe, że witryna jest błędnie skonfigurowana lub osoba atakująca naruszyła połączenie." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "É possível que o site esteja mal configurado ou que um invasor tenha comprometido a tua ligação." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Возможно, неверно задана конфигурация сайта либо ваше соединение перехвачено злоумышленником." + } + } + } + }, + "ssl.error.page.advanced.info.title" : { + "comment" : "Title of the Advanced info section shown in an error page that warns users of security risks on a website due to SSL issues.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo warnt dich, wenn eine Website ein ungültiges Zertifikat hat." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo warns you when a website has an invalid certificate." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo te avisa cuando un sitio web tiene un certificado no válido." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo vous avertit lorsque le certificat d'un site Web n'est pas valide." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo ti avverte quando un sito web ha un certificato non valido." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo waarschuwt je wanneer een website een ongeldig certificaat heeft." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo ostrzega gdy witryna internetowa ma nieprawidłowy certyfikat." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo avisa-te quando um site tem um certificado inválido." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo предупредит вас, если у сайта окажется недействительный сертификат." + } + } + } + }, + "ssl.error.page.body" : { + "comment" : "Error description shown in an error page that warns users of security risks on a website due to SSL issues. %1$@ represent the site domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Zertifikat für diese Website ist ungültig. Möglicherweise stellst du eine Verbindung zu einem Server her, der vorgibt, %1$@ zu sein, wodurch deine vertraulichen Daten in Gefahr sein könnten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The certificate for this site is invalid. You might be connecting to a server that is pretending to be %1$@ which could put your confidential information at risk." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El certificado de este sitio no es válido. Puede que te estés conectando a un servidor que se hace pasar por %1$@, lo que podría poner en riesgo tu información confidencial." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de ce site n'est pas valide. Vous vous connectez peut-être à un serveur qui se fait passer pour %1$@, ce qui pourrait mettre vos informations confidentielles en danger." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di questo sito non è valido. Potresti collegarti a un server che finge di essere %1$@ e che potrebbe mettere a rischio le tue informazioni riservate." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het certificaat voor deze website is ongeldig. Mogelijk maak je verbinding met een server die zich voordoet als %1$@, waardoor je vertrouwelijke informatie in gevaar kan komen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat tej witryny jest nieprawidłowy. Być może łączysz się z serwerem podszywającym się pod %1$@, co może narazić poufne informacje na niebezpieczeństwo." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O certificado deste site é inválido. Podes estar a estabelecer ligação a um servidor que finge ser %1$@, o que pode colocar as tuas informações confidenciais em risco." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сертификат этого сайта недействителен. Возможно, вы пытаетесь подключиться к серверу, который выдает себя за %1$@, что ставит под угрозу вашу конфиденциальную информацию." + } + } + } + }, + "ssl.error.page.header" : { + "comment" : "Title shown in an error page that warn users of security risks on a website due to SSL issues", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warnung: Diese Website ist möglicherweise unsicher" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warning: This site may be insecure" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertencia: este sitio puede ser inseguro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertissement : ce site n'est peut-être pas sécurisé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attenzione: questo sito potrebbe non essere sicuro" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwing: Deze website is mogelijk onveilig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzeżenie: ta witryna może być niebezpieczna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso: este site pode ser inseguro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Внимание! Возможно, сайт небезопасен" + } + } + } + }, + "ssl.error.page.leave.site.button" : { + "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to leave the website and navigate to previous page.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Website verlassen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Leave This Site" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir de este sitio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitter ce site" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esci da questo sito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze website verlaten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opuść tę stronę" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deixar este site" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Покинуть сайт" + } + } + } + }, + "ssl.error.page.tab.title" : { + "comment" : "Title shown in an error page tab that warn users of security risks on a website due to SSL issues", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warnung: Website ist möglicherweise unsicher" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warning: Site May Be Insecure" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertencia: el sitio puede ser inseguro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertissement : le site n'est peut-être pas sécurisé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attenzione: il sito potrebbe non essere sicuro" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwing: Website mogelijk onveilig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzeżenie: witryna może być niebezpieczna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso: o site pode ser inseguro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Внимание! Возможно, сайт небезопасен" + } + } + } + }, + "ssl.error.page.visit.site.button" : { + "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to visit the website anyway despite the risks.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Risiko akzeptieren und Website besuchen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Accept Risk and Visit Site" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceptar el riesgo y visitar el sitio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accepter le risque et visiter le site" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accetta il rischio e visita il sito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Risico accepteren en site bezoeken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaakceptuj ryzyko i odwiedź witrynę" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceitar o risco e visitar o site" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Принять риск и перейти на сайт" + } + } + } + }, + "ssl.error.self.signed.message" : { + "comment" : "Warns the user that the site's security certificate is self-signed and not trusted. '%1$@' is the site's domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Sicherheitszertifikat für %1$@ wird vom Betriebssystem deines Geräts als nicht vertrauenswürdig eingestuft." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The security certificate for %1$@ is not trusted by your device's operating system." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El sistema operativo del dispositivo no confía en el certificado de seguridad de %1$@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de sécurité de %1$@ n'est pas approuvé par le système d'exploitation de votre appareil." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di sicurezza di %1$@ non è considerato attendibile dal sistema operativo del tuo dispositivo." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het besturingssysteem van je apparaat vertrouwt het beveiligingscertificaat voor %1$@ niet." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat bezpieczeństwa %1$@ nie jest zaufany przez system operacyjny urządzenia." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O sistema operativo do teu dispositivo não confia no certificado de segurança de %1$@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Операционная система вашего устройства не доверяет сертификату безопасности сайта %1$@." + } + } + } + }, + "ssl.error.wrong.host.message" : { + "comment" : "Explains an SSL error when a site's certificate doesn't match its domain. '%1$@' is the site's domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Sicherheitszertifikat für %1$@ stimmt nicht mit *.%2$@ überein." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The security certificate for %1$@ does not match *.%2$@." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El certificado de seguridad de %1$@ no coincide con *.%2$@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de sécurité de %1$@ ne correspond pas à *.%2$@." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di protezione per %1$@ non corrisponde a *.%2$@." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het beveiligingscertificaat voor %1$@ komt niet overeen met *.%2$@. " + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat zabezpieczeń %1$@ nie jest zgodny z *.%2$@." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O certificado de segurança de %1$@ não corresponde a *.%2$@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сертификат безопасности сайта %1$@ не подходит для домена *.%2$@." + } + } + } + }, "Start Speaking" : { "comment" : "Main Menu Edit-Speech item", "localizations" : { @@ -52312,4 +53135,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 148900fcec..e17e162d79 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -799,11 +799,12 @@ final class AddressBarButtonsViewController: NSViewController { guard let host = url.host else { break } let isNotSecure = url.scheme == URL.NavigationalScheme.http.rawValue + let isCertificateValid = tabViewModel.tab.isCertificateValid ?? true let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig let isUnprotected = configuration.isUserUnprotected(domain: host) - let isShieldDotVisible = isNotSecure || isUnprotected + let isShieldDotVisible = isNotSecure || isUnprotected || !isCertificateValid privacyEntryPointButton.image = isShieldDotVisible ? .shieldDot : .shield diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index 6ada06456b..48eedb91f4 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -85,6 +85,9 @@ extension Tab: NavigationResponder { // Tab Snapshots .weak(nullable: self.tabSnapshots), + // Error Page + .weak(nullable: self.errorPage), + // should be the last, for Unit Tests navigation events tracking .struct(nullable: testsClosureNavigationResponder) ) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index bac1e48b47..a1e4228051 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -340,6 +340,8 @@ protocol NewWindowPolicyDecisionMaker { private let statisticsLoader: StatisticsLoader? private let internalUserDecider: InternalUserDecider? let pinnedTabsManager: PinnedTabsManager + private let certificateTrustEvaluator: CertificateTrustEvaluating + var isCertificateValid: Bool? private(set) var tunnelController: NetworkProtectionIPCTunnelController @@ -382,7 +384,8 @@ protocol NewWindowPolicyDecisionMaker { canBeClosedWithBack: Bool = false, lastSelectedAt: Date? = nil, webViewSize: CGSize = CGSize(width: 1024, height: 768), - startupPreferences: StartupPreferences = StartupPreferences.shared + startupPreferences: StartupPreferences = StartupPreferences.shared, + certificateTrustEvaluator: CertificateTrustEvaluating = CertificateTrustEvaluator() ) { let duckPlayer = duckPlayer @@ -421,7 +424,8 @@ protocol NewWindowPolicyDecisionMaker { canBeClosedWithBack: canBeClosedWithBack, lastSelectedAt: lastSelectedAt, webViewSize: webViewSize, - startupPreferences: startupPreferences) + startupPreferences: startupPreferences, + certificateTrustEvaluator: certificateTrustEvaluator) } @MainActor @@ -451,7 +455,8 @@ protocol NewWindowPolicyDecisionMaker { canBeClosedWithBack: Bool, lastSelectedAt: Date?, webViewSize: CGSize, - startupPreferences: StartupPreferences + startupPreferences: StartupPreferences, + certificateTrustEvaluator: CertificateTrustEvaluating ) { self.content = content @@ -467,6 +472,7 @@ protocol NewWindowPolicyDecisionMaker { self.interactionState = interactionStateData.map(InteractionState.loadCachedFromTabContent) ?? .none self.lastSelectedAt = lastSelectedAt self.startupPreferences = startupPreferences + self.certificateTrustEvaluator = certificateTrustEvaluator let configuration = webViewConfiguration ?? WKWebViewConfiguration() configuration.applyStandardConfiguration(contentBlocking: privacyFeatures.contentBlocking, @@ -1219,7 +1225,13 @@ protocol NewWindowPolicyDecisionMaker { webView.publisher(for: \.serverTrust) .sink { [weak self] serverTrust in - self?.privacyInfo?.serverTrust = serverTrust + guard let self else { return } + self.isCertificateValid = self.certificateTrustEvaluator.evaluateCertificateTrust(trust: serverTrust) + if self.isCertificateValid == true { + self.privacyInfo?.serverTrust = serverTrust + } else { + self.privacyInfo?.serverTrust = nil + } } .store(in: &webViewCancellables) @@ -1478,10 +1490,10 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift invalidateInteractionStateData() - if !error.isFrameLoadInterrupted, !error.isNavigationCancelled, - // don‘t show an error page if the error was already handled - // (by SearchNonexistentDomainNavigationResponder) or another navigation was triggered by `setContent` - self.content.urlForWebView == url { + if !error.isFrameLoadInterrupted, !error.isNavigationCancelled, error.errorCode != NSURLErrorServerCertificateUntrusted, + // don‘t show an error page if the error was already handled + // (by SearchNonexistentDomainNavigationResponder) or another navigation was triggered by `setContent` + self.content.urlForWebView == url { self.error = error // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML diff --git a/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift new file mode 100644 index 0000000000..aac116210a --- /dev/null +++ b/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift @@ -0,0 +1,180 @@ +// +// SSLErrorPageTabExtension.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 Navigation +import WebKit +import Combine +import ContentScopeScripts +import BrowserServicesKit + +protocol SSLErrorPageScriptProvider { + var sslErrorPageUserScript: SSLErrorPageUserScript? { get } +} + +extension UserScripts: SSLErrorPageScriptProvider {} + +final class SSLErrorPageTabExtension { + weak var webView: ErrorPageTabExtensionNavigationDelegate? + private weak var sslErrorPageUserScript: SSLErrorPageUserScript? + private var shouldBypassSSLError = false + private var urlCredentialCreator: URLCredentialCreating + private var featureFlagger: FeatureFlagger + + private var cancellables = Set() + + init( + webViewPublisher: some Publisher, + scriptsPublisher: some Publisher, + urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), + featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { + self.featureFlagger = featureFlagger + self.urlCredentialCreator = urlCredentialCreator + webViewPublisher.sink { [weak self] webView in + self?.webView = webView + }.store(in: &cancellables) + scriptsPublisher.sink { [weak self] scripts in + self?.sslErrorPageUserScript = scripts.sslErrorPageUserScript + self?.sslErrorPageUserScript?.delegate = self + }.store(in: &cancellables) + } + + @MainActor + private func loadSSLErrorHTML(url: URL, alternate: Bool, errorCode: Int) { + let domain: String = url.host ?? url.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true) + let html = SSLErrorPageHTMLTemplate(domain: domain, errorCode: errorCode).makeHTMLFromTemplate() + webView?.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) + loadHTML(html: html, url: url, alternate: alternate) + } + + @MainActor + private func loadErrorHTML(_ error: WKError, header: String, forUnreachableURL url: URL, alternate: Bool) { + let html = ErrorPageHTMLTemplate(error: error, header: header).makeHTMLFromTemplate() + loadHTML(html: html, url: url, alternate: alternate) + } + + @MainActor + private func loadHTML(html: String, url: URL, alternate: Bool) { + if alternate { + webView?.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) + } else { + webView?.setDocumentHtml(html) + } + } + +} + +extension SSLErrorPageTabExtension: NavigationResponder { + @MainActor + func navigation(_ navigation: Navigation, didFailWith error: WKError) { + let url = error.failingUrl ?? navigation.url + guard navigation.isCurrent else { return } + guard error.errorCode != NSURLErrorCannotFindHost else { return } + + if !error.isFrameLoadInterrupted, !error.isNavigationCancelled { + // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML + guard let webView else { return } + let shouldPerformAlternateNavigation = navigation.url != webView.url || navigation.navigationAction.targetFrame?.url != .error + if featureFlagger.isFeatureOn(.sslCertificatesBypass), + error.errorCode == NSURLErrorServerCertificateUntrusted, + let errorCode = error.userInfo["_kCFStreamErrorCodeKey"] as? Int { + sslErrorPageUserScript?.failingURL = url + loadSSLErrorHTML(url: url, alternate: shouldPerformAlternateNavigation, errorCode: errorCode) + } + } + } + + @MainActor + func navigationDidFinish(_ navigation: Navigation) { + sslErrorPageUserScript?.isEnabled = navigation.url == sslErrorPageUserScript?.failingURL + } + + @MainActor + func didReceive(_ challenge: URLAuthenticationChallenge, for navigation: Navigation?) async -> AuthChallengeDisposition? { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return nil } + guard shouldBypassSSLError else { return nil} + guard navigation?.url == webView?.url else { return nil } + guard let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else { return nil } + + shouldBypassSSLError = false + return .credential(credential) + } +} + +extension SSLErrorPageTabExtension: SSLErrorPageUserScriptDelegate { + func leaveSite() { + guard webView?.canGoBack == true else { + webView?.close() + return + } + _ = webView?.goBack() + } + + func visitSite() { + shouldBypassSSLError = true + _ = webView?.reloadPage() + } +} + +protocol ErrorPageTabExtensionProtocol: AnyObject, NavigationResponder {} + +extension SSLErrorPageTabExtension: TabExtension, ErrorPageTabExtensionProtocol { + typealias PublicProtocol = ErrorPageTabExtensionProtocol + func getPublicProtocol() -> PublicProtocol { self } +} + +extension TabExtensions { + var errorPage: ErrorPageTabExtensionProtocol? { + resolve(SSLErrorPageTabExtension.self) + } +} + +protocol ErrorPageTabExtensionNavigationDelegate: AnyObject { + var url: URL? { get } + var canGoBack: Bool { get } + func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) + func setDocumentHtml(_ html: String) + func goBack() -> WKNavigation? + func close() + func reloadPage() -> WKNavigation? +} + +extension ErrorPageTabExtensionNavigationDelegate { + func reloadPage() -> WKNavigation? { + guard let wevView = self as? WKWebView else { return nil } + if let item = wevView.backForwardList.currentItem { + return wevView.go(to: item) + } + return nil + } +} + +extension WKWebView: ErrorPageTabExtensionNavigationDelegate { } + +protocol URLCredentialCreating { + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? +} + +struct URLCredentialCreator: URLCredentialCreating { + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + if let trust { + return URLCredential(trust: trust) + } + return nil + } +} diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 56351d7c11..b56da3410d 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -185,6 +185,11 @@ extension TabExtensionsBuilder { scriptsPublisher: userScripts.compactMap { $0 }, webViewPublisher: args.webViewFuture) } + + add { + SSLErrorPageTabExtension(webViewPublisher: args.webViewFuture, + scriptsPublisher: userScripts.compactMap { $0 }) + } } } diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 017c0c6716..c34baac57b 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -46,6 +46,7 @@ final class UserScripts: UserScriptsProvider { let autoconsentUserScript: UserScriptWithAutoconsent let youtubeOverlayScript: YoutubeOverlayUserScript? let youtubePlayerUserScript: YoutubePlayerUserScript? + let sslErrorPageUserScript: SSLErrorPageUserScript? init(with sourceProvider: ScriptSourceProviding) { clickToLoadScript = ClickToLoadUserScript(scriptSourceProvider: sourceProvider) @@ -64,14 +65,16 @@ final class UserScripts: UserScriptsProvider { autoconsentUserScript = AutoconsentUserScript(scriptSource: sourceProvider, config: sourceProvider.privacyConfigurationManager.privacyConfig) + sslErrorPageUserScript = SSLErrorPageUserScript() + + specialPages = SpecialPagesUserScript() + if DuckPlayer.shared.isAvailable { youtubeOverlayScript = YoutubeOverlayUserScript() youtubePlayerUserScript = YoutubePlayerUserScript() - specialPages = SpecialPagesUserScript() } else { youtubeOverlayScript = nil youtubePlayerUserScript = nil - specialPages = nil } userScripts.append(autoconsentUserScript) @@ -80,11 +83,14 @@ final class UserScripts: UserScriptsProvider { contentScopeUserScriptIsolated.registerSubfeature(delegate: youtubeOverlayScript) } - if let youtubePlayerUserScript { - if let specialPages = specialPages { + if let specialPages = specialPages { + if let sslErrorPageUserScript { + specialPages.registerSubfeature(delegate: sslErrorPageUserScript) + } + if let youtubePlayerUserScript { specialPages.registerSubfeature(delegate: youtubePlayerUserScript) - userScripts.append(specialPages) } + userScripts.append(specialPages) } #if SUBSCRIPTION diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index c6853be3fb..5c03b57c92 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -20,6 +20,7 @@ import BrowserServicesKit import Cocoa import Combine import Common +import WebKit final class TabViewModel { @@ -203,6 +204,7 @@ final class TabViewModel { .sink { [weak self] _ in self?.updateTitle() self?.updateFavicon() + self?.updateCanBeBookmarked() }.store(in: &cancellables) } @@ -293,7 +295,11 @@ final class TabViewModel { switch tab.content { // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors case _ where isShowingErrorPage && (tab.error?.code != .webContentProcessTerminated || tab.title == nil): - title = UserText.tabErrorTitle + if tab.error?.errorCode == NSURLErrorServerCertificateUntrusted { + title = UserText.sslErrorPageTabTitle + } else { + title = UserText.tabErrorTitle + } case .dataBrokerProtection: title = UserText.tabDataBrokerProtectionTitle case .settings: @@ -326,10 +332,9 @@ final class TabViewModel { private func updateFavicon(_ tabFavicon: NSImage?? = .none /* provided from .sink or taken from tab.favicon (optional) if .none */) { guard !isShowingErrorPage else { - favicon = .alertCircleColor16 + favicon = errorFaviconToShow(error: tab.error) return } - switch tab.content { case .dataBrokerProtection: favicon = Favicon.dataBrokerProtection @@ -368,6 +373,13 @@ final class TabViewModel { updateAddressBarStrings() } + private func errorFaviconToShow(error: WKError?) -> NSImage { + if error?.errorCode == NSURLErrorServerCertificateUntrusted { + return .redAlertCircle16 + } + return.alertCircleColor16 + } + // MARK: - Privacy icon animation let trackersAnimationTriggerPublisher = PassthroughSubject() diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index 22b6ce65aa..99dce527a4 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -780,4 +780,96 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window2.firstResponder, window2) } + func test_WhenSiteCertificateNil_ThenAddressBarShowsStandardShieldIcon() async throws { + // GIVEN + let expectedImage = NSImage(named: "Shield")! + let evaluator = MockCertificateEvaluator() + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), certificateTrustEvaluator: evaluator) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! + XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) + } + + func test_WhenSiteCertificateValid_ThenAddressBarShowsStandardShieldIcon() async throws { + // GIVEN + let expectedImage = NSImage(named: "Shield")! + let evaluator = MockCertificateEvaluator() + evaluator.isValidCertificate = true + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), certificateTrustEvaluator: evaluator) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! + XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) + } + + func test_WhenSiteCertificateInvalid_ThenAddressBarShowsDottedShieldIcon() async throws { + // GIVEN + let expectedImage = NSImage(named: "ShieldDot")! + let evaluator = MockCertificateEvaluator() + evaluator.isValidCertificate = false + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), certificateTrustEvaluator: evaluator) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! + XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) + } +} + +protocol MainActorPerformer { + func perform(_ closure: @MainActor () -> Void) +} +struct OnMainActor: MainActorPerformer { + private init() {} + + static func instance() -> MainActorPerformer { OnMainActor() } + + @MainActor(unsafe) + func perform(_ closure: @MainActor () -> Void) { + closure() + } +} + +extension NSImage { + func pngData() -> Data? { + guard let tiffRepresentation = self.tiffRepresentation, + let bitmapImage = NSBitmapImageRep(data: tiffRepresentation) else { + return nil + } + return bitmapImage.representation(using: .png, properties: [:]) + } + + func isEqualToImage(_ image: NSImage) -> Bool { + guard let data1 = self.pngData(), + let data2 = image.pngData() else { + return false + } + return data1 == data2 + } +} + +class MockCertificateEvaluator: CertificateTrustEvaluating { + var isValidCertificate: Bool? + + func evaluateCertificateTrust(trust: SecTrust?) -> Bool? { + return isValidCertificate + } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 0cf9ebe7c0..0eb8a47757 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", exact: "134.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 7b5cbdc07d..f7a52283d8 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/Screenshot 2024-03-14 alle 13.19.29.png b/LocalPackages/Screenshot 2024-03-14 alle 13.19.29.png new file mode 100644 index 0000000000000000000000000000000000000000..7ddccad49d7ffbd57f21a9fcb0ca98e55e568383 GIT binary patch literal 85134 zcmeGEc|6qJ|38i+MUf>T*;6TND8?8RWkSeh-^-Hh+hFXeP+=x}Vn`FR@B5w@OZJ^% z>|Avpym zAtCdmJOex#Pmr)BA)%JGl9zw1C@;_T*xBKkm8}H{$-}o%IuyDZ%}faf|8N(Qko_a) z@bck#u4}KE?t0SOzP@_*2KSkl@7Rqr9hXf>S8-L# zZxMblnCvbq&eu}(^ zH^LW5KefLYl#q}>(mL^d7w?2p=RHb{-F~Ioh`Zerc)JW6!kzgc7~ACk&D)8_efU@p zacoy6*)uKNm_tN$hO)qCNcpi9Zml;jyM9)0B& zPCRdO2FC3+>2j)`Tl_UhH7-^YF#-HO z3d|PyJQO}xyLI)BnCLL)?sJo#^U1trbb&2nQ@E{-GhVn@Qlsgu=H#i8FvQm*S~R=q`QvAk{BOckTO4SD80dWTuQ}PU#?C{dr{ES!X%( zN|x{0y8%uud2mxw!}sTWAV!9C>^V&g!Yz}55BS&XV*Dv<&-lL}{gRvP4|#w3byn3@ z)tXWl19j$==J}7P>pePZIF{82p_NLBnM5=s z7X2@Wx>VS6*5wnVrL>wsEu}*CBy5g4mx2QJ>zeG}Tkvm0YXuM@wsFta>PH{%smI^A zVR5M;(R5?u;T@?vCESd9S$>(wD+7f|o&`akzKD*aq&;sq&G8U}!5jAP?PjTY!Gj{1 z9Jg{KbWioaWRICh>f5?sU491L(6Z4S2>#~xowc$cUujmV53~jHQQu^wOlcYGdV%6i zxK97foavVCS}@lW$NLZrJ+&pTr4ETXj=5oRQ6=P{!hf#V8$WwU`l6%R-`^jNE6a@_ zS=xDlJPuopKhNL`)+8x)e9aqmkq>0(##LML8tmP@=`V7NLflcNy_Ow*%8!ox+ue(A zD4L&K`60uQ?Q-@5*|QgApU5NqEuP%br4{$jdcr$+ispr>!I$%8uV7cCn{1{T-X*1f6}Nt^pTwhny+y9R3l1au=&z~}+_^h<0Vb#) zu%fWo*)wOcd41Tw`tH8Si?k=@A8yXR5D^h)3P^r3|A9O%Aj)(`mr*)ZbOv_ygH3&AAdHz`7rg7wr zbBUs%`H}gd-4Dy;=0AI23&7t%ut>koNP4}+}|5Ty(0u|7DT?bY-3(1Br=-*kUUglnYb=jy* zJrv!j6PKl(q@C)fvYZTlF!~QFSw_9;VU^+Gr#;3k|QtZAFkN9H>fMGy70_3!C>XYJ}66tzE>PBDYIfaBWH9cTx1VzRpxLs3?d z}5N7Cl!8K(;bB=9bzOwuy#3?(C7TaV20uS7 zs?R>}l_O!;VWRd)C%r^xHD!#~C(!r3Z}E}yq30pRnNok$y^y+#uR_mQp7#v0q21wV zXEg>N)UH(oeoaC>&76SMX9Wrd9x5~{a4VRiT@@@tJVRYW4?_z=bXXE^t%xMW5kFF@ zXvfCi_Gs^XntMqyq$uQ>LSkBy)z`FMSg=*byrq~;0eOLGLDe(bA8E6XHeefCvnsPb zOxKyr1fJ>k>SBw&=!y!82@dEclz2N>*hQHa)uxZ7kLY)NG&3?cGV{R&KAW->8%k-J z4HWE(wu-Z>sM}6wx5ht%-0jcS_bXndpK49&OlnN( zQ6CWbmN%F;MwodR9Cni@1__Z}kBf-YK=g>GdZ7yy0z&yX`alas1t_tOvTwVARBhJ= zEsB+2westD>G&@y42;;rN<{3buphD2* zfw%22OW>@+R-LP)fgjE+kE52WS}VK?x1QI{*`Yc2KkPdC6cb@YQEv?&iG4R>?4HwM z`T@so-2TR4$-b=FP=3B{g`V)Kc(SXcL8rzFss_b|Qpxga-`956_H$)@di`nZ9__2P zmtrq%Uhd0bR7$ih%hri?mkGEDe`2ZdKkvr?yw^q8gHG#acbmc02s9C4T zQ!{Nu8K)J?&3BndGWGbcQR=jxv(UUy&QR{g`xwpdkWh9;DV}X_Hfj5Ru-T>V1X6{itKkCEm4TT~M73>uczroL#UY!L=4?^H5- z`yKK5B5R?!a%%#kygm;+-2>GcTS5{Z)%;lhVa#n%YE0zgzip$@zrXZ#iGAplq@tvk zOLJLT8T7vXXIpib1gEjyO7Xm^()(DEA)EDuW}k4!=56n*esaF@{^AE=4q?L&{leUs z_L#IU`hExp&5UcdrF~8l>{&_?{J5QYqjauho(LP;Okqh8n{A(qZ~j>(%+b>^y{C8feP!R&Y z1!M&qGG!TTW<{EKru|6EtXyg}9h+3HLF~CM=bTdRjwuv!H7R#uaf*FDlr`hq>Ym+c zs9rcpXc6IG)E3c>%J!`A>|O|-$)1Vpe?0JT0G1i5JCfUOI-2il(r=kvqeuTZucSNI zS$$^z{K5GF@jZvkQS-ty+n&_6Z*3(duWi#Z^3(ZnN?85Ut@+~bc6dLtBkiiuCtgF_ zIa2qfz;2Bzw%OZXv&Sq-ZLH4g^JL8rN&D{fN&97fSNeQE-YfV#7ZNozY9;)&f1 zy$l;_)bRq~AwMPW^c`5^JVqtr;|hw!YgWPKk;~%HS^;zW=|m})1lObK(}`3VAx|TS z^IMO!qx}7ua;B2#`kIPE-_pLSqLp`;sKAT_9o7+r5>)A|`|{>i?IPaZ(cZ9{#iL+Z z`&-t?^3;fw{f2`(uDNVu!G2=2V`mo2ixu|yMEG=&G*O=}YkNF6GBM^qmD&; zAw7^LkY>O2WSb+6;huHN>mBJ8s0!p3`HrEF%!YAVRk=}0i>`YS4!Lju` z>&U>C(z^Bd)}+LqgIlGm&qy!l=<%A-N=3)2vh1AS;BMG1;UM*kT8E6;oN=SLapKD8{#FNS-&7bWR+FfK~I3BUnEV7PQb4)pYZqk#SNa zMQg{>Da=Qsg{J^FhV{MYvqd1e-$U|dhx}`B>tj}IM)OXTD9<;a+S$1rt1_!`+y~Q+ zUHLKDME^&9j0fIF%=T8^hL?^fn}PMjI}2UKXDTWre84p&2|4LylGDHyDe#pcW&Pi^ z0_hEsQ$L@Rk&pygk&yo`qY9i)KJS6=Nu6KkQ{jOm6u{jD;0ymm_HXG^;7_Oiy(aSn z-jUqXkXKX$&KhRU78dp{&mCZWbvK>@56(J1)N>&rVY+tmB~|?A>Kf2~zm=vgOjqR* z*v!FB=&8Absf7^S&hexj5(pd&T-sT{o^rwMZ0%jZaOo>QOMrpvlgGkWxPBIa*+^f} zRe8)M@8E2~B`$PJ=++gPb6i|p5NGpe;D7Eb{H_k%Nnd#mgE@kQh27oVh1|u29GoqM zMIQ)pi;4;YB?MhO>|sygg7z*vzZ&_so%F9r-Uvyf)t^VuD-sShOfB_1hyb%@=x+VO-wt=dUlc(UvR&Wbjz57;n0MCFvWW>d8 zLw=V3f8PAp;~zD3|EmcS75}s5A8-C|O)VD-XL$!Zpi7v{f8zRG`OlZXD?)@%M*asU zezE!IQ-IGh=ODuWlbOsp@>AFoRbsQcucir{0VO;6kV*r8Zu~l*T&J;#2j-=cklZCv zynjyDjFOvs;}?k%}&FFWx*WQS@26v~=`ChDzxrZx6fL zCy7fq&qLXRjt%Xss%^>r_<|0ZwLcVRiAU+4`L_P-m~#AK~ET?c3Zj`+)DPB2v!ozx_t zvD9}jn2v9kmyDj8T-TVnhR=g(^O{-XP!Yx+-5CgNedU2j)tRizCANB6&}sRfUD8C| zWkbm~u+E&2lY55D`D%K)ae1qvf{2G}+-;N6lHw?Z%*R_d)aKuXIoR=s!e#G?tM`{= zWPjAz$vB8Vj7De=vl)e*^{1osrvqICh@m-c6B!85R?AKQ-lbEcMj+8ykR_R(q44<* z(YAeTUCUUsD1I|y)uK>%S3kjLow7S={fh_3e#YxGuud1l&`Cp-{-Iatpq4xp*AxOy zh8gsNY5&eh&!Tv~Go_}gXqN7JLaKAOJV(-u!24|PVwzGaiq=^PVr12%l3cQ|A=;wzC;_INvmS{E zArsAl&8+guP^0$qcq~q2%tihelWs)-#0afeRoTTj*tIIIHB_-tPfO!=hN9!mAKILr z*T#ku3d@|0pILjgsP-kpl{(@pUFTypbj*J@cApE;?!T7o_*0w0UPxWkudMJ~x7pMD zxM}5NYz7;+J+GseW0KF83TgJNUf0iSO_Q1ak{a!_`*C16rahrrJNC;@u29;tpNwxH z+3GPczOV~VM6v?5^55Z`loaw{d({pRCOc6n`QM$H%rjyLc2QB@eO)c?C?VX4{k(#k zmbCPtBJEGHQTm?-+F?0!q5_T|$kQ3}vdcR&#%W&0Qzk1atShZ9D<|ldL$Zpbp{k-q zN72>VA6rV+!(OofAx`3&j_Who69xmvvkPy~Vhoz!Pwt-3k}~ZHbPmvevC+#c zzly1L81PxYIQ^(;R-dLU8ZytfIHI)J&{X_#TBkXVucy05*&rq%;poe6sb2&}!`jC# za-Cdm{62#tzOKY7a@EGbsg3#dM>S7}JmVgN{cLxonDsY#Px&6xCbjhKbttiq9iy{X z*Yq#6ov?w9%;pN9fGS+*lSU%x9eH1;3>+GNOl_5XUvM{pVqWu6LX3L3sqo@Rm8cMU zIl6uvP>85R){2BOi!02(Im1u)23@LE!=okxcgY`vQ*9|PJ zhruvS)X*=n4=MqIE6z9Q`3l&ZJa_G6Vt{y(eMAbR_^ejrhIEI8!8|&@y(U@}M%NTk zeHlrnHfv*$7sI2n7g?huV3ZLV8*WrmomL+HYq*Y|&wlTc>>6dB0z3;%z>9dYn>qVR zIs(ua58(Va5+TjKw{h<5)ww%%F;;dZ=20oSGttY+*~|m$6j6&&JDX*$p0T#SBpA$$5VW|I+C;n!3EYn5#HqFh~ccjva9a%zjBJHK|UHiuP5!A&F*A^iaeXHBTQd^?SHvsN4Y0i8?D)mpPYJBk_u(w_U-K5R4wA6m z{9*UQ;b(>8>>3=x> zT@oxMB~{O$1`&ZA5vOdv(`f-C=@YpDHE{w^h(5UHe8rQ~k9rLn8I@Xf(X(4ZKaHTSkVIt0m_voRK%UcSBENO(mur_Bzd`AJe7rxg7D2(mmf+e~Kyz3E#;nZE#d35tJA zQ1JLmbgy49yg^Uh&S(EakpV8|G6EjJ_JKqS{eSxXlewx~2t-el+uv;f(-k0gFphpu z_&+QG0l`g`XXO91VQ>$aRA|G@F4OX zzutp($r0GDd2T6ciYC{_uK6%E&%eZI%jwVK3d0gQi%mPN4G3T-OqU*S+uXUo_4vBu z7lbNRK>X!~@UCjpZXW|@mnjg~Me%{qU$(0%^RqE^g553a#1EaG^!|874n5qn8pg@F z!mn2a|0-M@Q5z-+-Z>^Fu}ccCd?0Qce$Nt3NU>{}3q^~?*iZj<2fw`P_%s8E2})rL z;wG1rwcKexa;IqM8Pb+a)-Ybb4b;Y)*if9pD|1cs{^?QzE17TL z%G#=7c*QYSrNs5Y4tDmghksWY`B`Oe?jt&jCWMWpp~*T3FQnu1TS=T~12-INt*TCW!NJ4qDHHgoyH$5hsI#EFn`Y^%gNk#eT^*S^i>= z%@xNbglel^Sj1TCIu*D681`#$0`? zz5Qyk`N?YU--GS?NxkQfJGk|tC3tZbnn+_|6m;NIf#>XrCInjePJPO#ZX`U!!BYe6$nrtp*NF$yyl9u( zE4Pm(xCo!g7Op;K-g4A=^MbvyN>H`y4bSnPq;D%e;bCVlvOv^}LC^T49wOS)Yx2}N z;Xg)MSALAo!5yJNnYEk4dtdQoo*ZC&)p6&86s{)!gix-u{^vC_VJIJP>oY)AI--#Ot4O;5!A zHKyj`XQjZ9bhn*R_YRJWl#^IP&-vsjGxHAbS+O`8#Kmnk_L-?Q$iZ6u)ReOtgz!4t zvH`Ntn32x#=dk3SFai^L?Ic$@i^Wm4mG(8t%l^2=HQhLhj5)YV}W9`kov7PFYxDF#!Oy%^oiW=_WwkC!Dlnv0jM?jbqBegWhuih@o z33b=(O^_wNlGV+a%6*6CVNEd|+iX-mf6@1N$IowP=CsoPaVQ!h4GyXl8mujSz7E#RRl>iUe9o;A^4W~2`_j*g=k=0X(yz{J##z*Th zw46t4K}B&yN=iqpRfwVcsB3*z$(Sd3(RRl8jEKY-d@5Uog~3O=8Q9Aiv<2iPUg6ho zI*L15#&Ik-7#BBmmCZyOY#&6Yl?-k?XwER+N}o`1@a&O1eC@Ieh{;{Thk;4BqVbU} z7S^M3N@s`BzUFY7!AA$r!@1kEHq_y(-KKZbcc5bI&e#y~JFkog)@{VD!h(fa;9xdz zF{j2Cc>~dYeY7vBX5RgDJcteZ8qG7_zIGkN*tgqo@Av>O>j%60kH(4kGvA)b#2;&Z zX9w%j;dp$wv-+`gMe1tM@?7S=#Dfa176qf}l9gw@r|&wbH<697v#*d9vVAuFmbbb- zIP5jHk=H!?#kcDMru@O4TSr+AK8v-9a<0Q0 z57AAT!eh6gLD9S3P1pvu=${ie-cW#wh<>yE+!8SOWMvZ6DpX<1`EmF`|MJ`#cqtK} zxgv@^3ax>PaIe`PtzR@b-ZPRK9ABwzlfYH-F1sY|VSY?2P&}vIje9GvL z^CRQuWNFxIIwp1xBG|<1y8LmLG8?hgN6U<2(*v{^a2 zCjoF~-0_AZ{;kwwD5LRiiw?p8^SL_u)g(=bZ9395_q}fezj4x%Q(cJIm)f{CqQ8`T z&>ZV&j#pC>vRm)KyI4CpVbTiS4+SKL)4Z~h3tXYNNG>$y%XPWix{kx!nv4I>D7ZfL#VW{z7rw%pYA+0(R6=jTyS ztPAX6P|V(G>CHsAB8BsUrISX2sR%u^JGJpu%hsgUmZbYavRY>y$YN!US@V4mwZHg) z$bADVF+2LWG5F#Tu+0DA9!rVataqBWiqDJw{E6n&Z;DDHS+FFq;+0i)ev=+v0$r3iadJsWNcef-W#Rd zAu|*OIP_0yTnN>~w*KjZIjHPqINPI%65qv=K)~4!4NE&#szUJu3@7JG_5O-c9Eh-c z<(b6Pd+`Od>XGhcUM>@^R&v!$UrZpw&5ja&B3dTC5Ksle1*=Z?1<>)j*6?$) zribc3+5P;$wQd_BwK7;=QfyMAktY6_vz>L>yzZY-8!a~3w$OPkm`v* z*1!{Q=*1+TusX8ehZlZCN89+w1`GGeb;Nk<#r6-n`BWRZ5{Q0-5GF9iExm+>$q`}L!}cjp?*gI)_ESb6hrGF$Mdv+(&c#D5 z*ZpebSy`sVgOsV)?QTPghJ5Nv^k|NweaV#Wmjzv z7Y&s^qSvt<^xvE(5%FpFUhMvq@z|^O=n7fckKb4Rj>R17DPq>efddDn@@e{aTpl=t2G$tUO zwqh<=`w8Up!0I#YGS5eXF3y?>N5nyljQF++;9r>%Tq_IXx#zW)4g)nB*yGd8G7vuI zsqt)m@{paKmonGAAL*PC_UsCFt=+*lj%I~=GGG%DH1RfVP|jOPGz zwPXMgA#Qt~HQrV-mXD~|!8|Nr44QBW=OLP=l;mbn@iQ5FZU#VAKsT+OS{27k@WbG` zf)`KJUki}>^!av&C2=s>_G&P{@?Fp7eTI77rD6Ak^ri7i_8Odl*Lp!`?E%+Te+($s z(SlU1mW4q%Du?RJ-ry&u>F`8W9fj7--T>;YC#-fs(z|_b?%rNgNkW$vQ`?SPp|Dc$%8R8@tt*p?m;6M?ANb^oT4?feIx6} zeRhX@3dANAlEMSEV%{(E0yP*siWRwE60e5d)T8a! z`xdg`%3F)SzP^b47I)D~EQv$M%ycRdc~)2h!}7ofVZ~0nVz}${DDf84o3n))4nF2M zswPenc;bg;p99V#2(v`g7`LZE45^Be^x9&s&<;PS*}~TOOlvY9Cy={WXp5mW%1A{m>96PSRGnl1>Ms z`{hE!;a^?7Jf_n}i+l&}6dAF2a2B=n1yBcrnd~{RvD~A~ZVC9{<@6X#ocE(#Vi%QQ zr(#t|oH6K)j7hIHN0EsE*5-JxG_0yUX*ns`yCz z{!*7It3Ox)Yn2FAy}9VB9oOt|I??ihCrZF{W`1+6mGL5O^W^9Z9RK?MC3aME(*%dBN zKe*J%R*<6*5R&ntLoF9q=7{`p9uX`Kw+*1Wsp$IVcMK8V%5ikORT_E1=A{dbC`Q@0 z3s)C;AEO{;`Ew5T8NmZ-o{QhwnS*`9V^jo??0E8uIXHq0`kIeQc?oODK10}0t9)0%pn@9Ok`!>5j&s#xbd zMVCYoG1Ie%61MN{z1Kh*Lm)`s3DjzwQOpSbF^tUeT!crX(O^%fzGaO1%*0gEY@75E zic@^oiI+9Qen=CEZ&92%9E8OIsZve`Ujc0??Ab_!bp$eYSi7Z%X|~V!tBZVxZ=&UG zj&wm8oQDs#u7}-~L!>^zbE#xdwYisoXTDo$Z+WFcI&rz*Bdx_8$OnxZ0^YOTB0uWp zmH~JX#??PRN6bi`r)2VhvM;<1eh`GKA}~}W>V{2;aQfZPDEefXRNEZWaIyV@h`$}` z`Or=SB9YRk%RfG-{H_hEcTujvV&QF>B?G4YLF;C1XlhHxJO-SQ zfvzVg96Mt_BYljXKQ&1boe%9f6jG?dc}|T9XFiFU=HnfLFb-@Nnpdf5F+00LhzH!# zT8)@TKIAGEr3TF(@U;7H7}EH(GXjV~*4D8{6-F4Upp0+#F*?b>~*Vc^#JBtarYr zUM(NJVW8sWP=7*yiqTSM65vI+8#HJtvV^$X5D?A&MQhZab>m`j-s2rA&Z0+Z67yA4DbWPq@D245 z-PXcw{9u1NmC*@blFHLVq`*xe*{C#;DGdkKOw5xgyJZ_hy6f)2C?DRcXiR(PX6&$p z%#o%VJ4VTbX<+JUtoOS7c;U%pQ^~Ws`u!7>>UUo@)JD6M1F){A3f$DE4-v(=?QtjjxHdN6oX%ob zGpCNAG)~Rjg;{Rvi{yVCqa2<|vP@0Y3FI@e8<11oU3_OF-OA*nLB%q3`V!a>%b@g8`qaStI%R^WS z-nQ|3N+B*LUPk%-p#A}MfW@OZR>axHvTuCuGzY{vBg(JLPhj4fs)mk9>>8v7H)R&mW7TaW@!dwNbGJ&?HNRiZi9QrS4>p zs1d)ogqwza`xe=7djFj7w~S!X>`t8LV%7!rsq!a;0F?0S^z+$Y`q2R|TYDe8Y3*fj z=?K(fN9|qbv*a1E-cKD)6WxEyk1l_c1x{rW4M&B?PR%_;n#QxkOQ#kOua=b&M188i zW&yTlC3q2!=qTI1qJw#be6Wt+G-K~@hFK@}#$5W*wG7sUiy3bXTCWmoSF{~{fAAC* zf&Gg(VcZthOGOJG<7UmiAseirHip`<{U6fYZUwD#tNATn7Fck&dUIeuRJE_!x99;j zJ#%F+Vhp&=V0Vq*!}hcK71^))SsKmca{}A+V8Bmjh)LyLgTq3hgBSa^h{J493e0S{ zvJlJa_}3#9#A360aj`0_dOVCRk4ei`$45}wHy<4$yaaJrxUb9n+=Yo-NJH(1G^;07 zSpsV7odDG(HtDLA5BP1pTkz*a3@j@j+o~4k_X>?z`U0w^FLvlXRBa{_9qmCPAg*(p zuY#;C9sE;7GZr>{cOKZysAKF~X=-lW#Q~rXyRFxq;U>G96=7*pdYCUe zii(Fv-_3J z7eJ0QbJkm}fT`7kd~=0QcHVMgWVFm)oW(I~FM76Zib5$Uf+|3FAe(+ZR>UElmCL?I zeKkXE<>QhyB8HxauJt{~YsvK*LW#ZyrX`)+w!IPP^ygJ~QjZ21hdkvngGE)pRX&US z0P*FooveK&yyVTT>FO1$-w}VJSOHHv&MaNe3A`!gL#ol5C(P86vAo$pBB~Ej^tCr+ z5j2SGZ$Jo}YvLnAN=C|d&!mrtf z(s$&H_+oF+x((i!22m91kXHLLrx!C=LUA6S*)WKX!>Hs<&!rQE93GJI`|2Ilm8T!u zMWaj4U@e(bQvIDo{BlrXSj9c(+g-`vp=>k7sfeRMs`7>byDOp91d@@M>dj=c8n|RL z95>+GMU2Rt9JaUZ+$D`YWRvYifyh;*{IOk<7ChsvCP(Wh$=a1-Pj&~BMM?0%_AHPH z9dTjC6M{H6eAhaT8rIfoj&$nV&5%pOUBjhIxWkJ2f}=Vdr_fQBjs8$j!Yq|0-_&%E zKDIom9P#z z#u(?tVz*DoF4X?X-Ina+oR-oabz$AdB^*5&W|`X_!S1bYB{2uD*7US_X<~56>t>%N zj`YO`2xH;gb}NJ)egGr@vODp9`5ZY8CI%QIR04XVH&w0;7&XA{m>HDKAKoX+W9$|a7!q#5DfwSTV-T$>k;TY@$m#!_-F5rgp4n=JsF)1ap@9>%b|OGZu%csrUkw{pYFi zyfdpYv@@PQ_eS!foKnUy-+PG>m6$?1AdD=wxlzVH17>TgdaE0vWGf3qj6Ch00V*2( z>Xd8O1Lo!K!!g<2jfo_`-HFjdEeRn2_Zx^i!u!b>=7RJ}zR~)wy~?ZR&#BChs>->o z>KLEP2%2~P$L93(d!2DYpz#y#ZzF*qD;H>J0Asnw>u)&7_m`TyHZ613yjIgiWZ=W# z)jjpBR$w0KmT6sYC^gD~TM^8DUuVw*a-5VnbE@1xJGY5c4m^da0SrK(y! zP^4}ZdC}YUhMj$7x{Tkn<-4Ham#81CS!QHRo?dJ|3bjcyJGvV2HX|tr3iCyf{g9wn zas@}s;JXMVuiP-Fh7XBoR@C5J%`#UTbNN!o*{Z2CBVSK>U7`i4e`N6V^zxBS&0O3R znV2fhj}^mYdc}7-+?ukpI~UXzRQ<$|!5h*cy?s*$!)bLVbCHp})Pcu%o<=^p{x%W&@>wrZ07+h$)z$)hQ7Ny5m&RX(>oG>4oyk4n!mo{Z@mD|xZF509C2~4vwM5*XlopJ; zKaDIA$VMP!^Bbuss&|Uxc(vl)yEK9}gnSlmY zgINiRrQ=cD;i%70O@;t#kiv_MACRM@PZ_HRUt?;c^!(TY_?r#}FPFWh8@x(ZZuX7s z-Ko_ina?vGL%&k#AWuMH5DB{qy-jEv$hTQs-%w7$6oQ76vy zKFadjcsnYQ>4Wb$>ROvSCe$oPJJ=mG?#tdXtcLTm`a@MuFc}DU=k2L4MVq|Kj+6pk zLMBC%(`(+os{}~%H5&gZu1;3WR#YeEslfvE&lKpOGLS1b=18?(1vY0cooswb+l?Vl zC{U$oro<;P z{BSKw!+{LrlI};Pe8#6}Xli)I8_3HdH0I6yG$=KpK~5Dd$ODOF_B@`GB$etZV0Mw3 z6vI2Li~AOMqn$?fcJ)Gf#&xA5zE*a2-!ExvTe5po6EQoZ=uvy`(HNPMxScH7-HyP- z!@lFP3fu^M46_O;9#Gw}Roe3=tJL*}`dxIioy5LTm{hr5i)`5CF~{0Ft2Gp#q>Mp= zoYbQ%fnvyb7TxzI`!DgrGR7X;2>CO$dSsBJWQG#4vb}|jpp&gYWxt&(P>hM(+QuqY z!h0i-)yT+w1QqRYKuDxU9=4gdTSO5;WUDOT{eF9#HWE&MC1IAh-%EkL9OK7O{V4Y5 zKgue2p!jc8!fXJpC!GEY?#J$tpVOkir=z&IisQAudIn{gtM9s7k5DyI2mP|U!j|($ z0KoWf2Jv<6JddL%8>+#bvOV6zymWgQNOqrhF@9V-l5@Z+6+RPIh}%Wstj0#e(Dkh( zjvvK;bj#%$r}xYZ zOOt=5k>ZueMpl7^fB|c}0i?VbZW~$yDHoS+cXEbDhB_=4lGTh%l)>Du>Lc3uaP3`Y z&odw^v%(_8y@48;HPh1pC!6#28wop*Yu~z>*-?5A zonVe|eRucA<21Aggt3NC1EWsJo=zDMe~!mj&)4iCk0);7;645;m_>c_$J+;A=FaVr_%+&H1EMu=ldezQ1!g3TE|4$dJBIL3We(Gf;WmKh{%j7 zsJIvlE;AXeQ}|gDcaQgvsinkx6XnZ-kC$u2Agg}FRl^6xzu>^XvvyBnYTYEk6>a(F zYtRwU0qP$IZ5GG^MF#hOjEUcv5fvGbz?Aij@19t@DABObepbZhAIz7C{AoK~kr#i# zh=0K#2A#>`Cgx!A5k7e_LP+L!?eWb91KSM+>JMpXg-M7r!hU7^b?hjcoGQ10uGoFK zm$=F)#6+5KwSBM4i3fpC)^RQ^*xC_SUG#Sqss136(tirTeBGxEmiR1q-dW!;S7)rJ zsOpBqkpizUkK;?5p?(|GF5&nz^U@*R;uNhk1nW}om+xp;Evq;1zv)0d7BhNKAq6<% zokof8MnF2vgS77I%pb|{+W9Mv-Uym_1DcSHoL$!%UVh17j1(9tP>tSr=?%r?v2Sl< zCDI7xbErg_{7nmTPx-i|qdHpOTi$pn91AKQB=tG`FPRhB*yHJFv!tZLt!C0}l@+X- z9=qiwLI=o~=BCP!xq<~tFKv7+;bXGLslWu=Z;wOW_*EMUr7TdN^td>X>lmEX+VwTdGu?uyak zvuwseE45S_M1PlmG8+q9czM=s zcWrE)y2+25spSd1EKum$-^i2N?Gqsc#n8jwYsi0ep6)F)jdC5*)@!C$?l2Vt26^ju z(ES%$w0zyM5YZm#!qYw*AELO#5BEr>=QmQNv*Oz^*Joq@boI{x1Fo10C{FWe^k$d_ zdN7thtFh_5f+J?uwq*>$9~VPU`FQeAcGC^|l0orvg)*1UnMm@O8lgUXT(!{m=tdJ2 zW3gTI@4x?v_~6bxBuBJ&rEyaCjebZd=vL10xl(3)@Z}&D+D>iBAtPq!wfqxp^U1o| z_mA*!l4N9}2YCX#nEvn_XV$T?oDh=gUhD!*J$1eL?;Zk9dgwyifP^?a-QCh(^$bIi zEfuOb-+V(h_Mq_Pzlpa$A)bh~(P&q6eI76CK_0c^gOlMe!uzqIq(S)%e}QcbRL1An z*hjn4K&fmO*Y#CZ`b3J3zaT(hjrYF+(f=YRM;?q5*-@@*k@8ByE|*c47WEMLI8Nn zHeIXgU)%wH^niAO!C6TCkEJnRVq}=!qvw!S#fi*2k z%4@W@e{ctK+_liIThFM)w!U}K=l(xR44}S>it#^yRcZxVqa;jre{T3u>KCoLdp)WJ z=#5T!+Vdv+!voK_z1v+9#Mazbt9EE4Mqxc`3pzEPaE#QLNXAm}S!n=5Qy(fZ1`=O1 z@w?N$PgdGdQ6RXEOB@}wXtoO@8-My zQSDhOgr6?XcYis#cxuRkt$G8s%7*8ez4mgrjexmi#B_-^#0sG{Tc-7LR(+NfK`?&7 zp5H`_?~1?`!Vn^IsEyfSk3l@5zF)Qk6=uT#nj>1-s8~}jCB9%o@iPHVNaD5K%Xp6B zs2oUl^l3@yW!#(qe^ui_&(d7(-6vh5yp>y74(!A!^KY+Uv5Rfhd`WBkf0p3}23DV;2iFfhQ73#F?Uwh+3JY z>CX=rby2C;{u#Mj>xV{fm%7LgN50~LHcmZb;V_bFuK|E3V<=(2T?aP+?93dz(K~-G zSkV%{0U$~eY8p`QPz~RF)h>$yxM+7M===1^R;&SSuvc{CtpeTLP`fW#Q1&^B&^G;@bUS0vBdvz8rIN-r z%?cXpi(6S0Sy0Jne4z=(iajEu(7=Zz2XNRPjpHZqnP-H7%Go>acp<33MC{0l@w8UDx2l>l!qaKgGid6aQe7^wvzyHPQpt6N2cVz zQv_mjxk)j19)~LcQ>pGQY~rMa_r#6emZ4G`?nyk_B+%!SRL%(EA$xVp)(L)i!g&t= zyU`ft8?q9QF+nn$NrYmM1-z1+JqwD2Uwngk{N^l#p~K95Yu6)T(QmjXK^*H?f4*XQ zJWOoq3KH|B^PbVc$vm>&G@Q*fT&;qLBD0&!yJ$PD)-k>zb!OpOZQl;&dxNY!x<{ym z-m~}}cMa*4LimQ%DGouo)b+3}Oj&xuO+=*DhaLsW$jcbeP8MF|b>JCU zpei8*1ArlTB_U4Q)`D6|7!!_A?|y!Kg8!P$>sYm(E7<|ItfqLbnZEO#w-HHJZH$mD zPuYGN!MQWnWq9AK7t^%5({*Q)uG7db7#rK?LEVq+P1LKt)hr1sB8mDiHd?s6BUHeK}7@-lujs0 z*>sQ+N+2L2B2pq9rS}p#B%y;MgeD~rN@73=H4th-i{KZ}Io~;FpL_4Wn`iyVkCnCN z9COZ*ImUS3cLvb}?<@P{Cy(Pq?fkF3^RFOE0HWBb`K@;B2m9ODR>aQJ9C@#gq&CcQ z;h~WVD-3*S}5U)=` zjj|%jdjrhqYl56*KEsmH+_teB@~`gg2dv8T_yb$ZWcU44GhPW!RtVgsUr==PQkon{ z0x>fW2?Fik)dM&Zk@H;Ct2!PRkN2p2gHl5$Hw@m}?eLvk976Y2)a5`Mg)F>|K!# z6`946y1DowyYIDONXIcS`@rPY-aHL|+gJ_DkmD!g&+SKEj+?&T zZF?PNq79jo1JnjP3EREEJ5gM*WZ4WCTVci!@OI~w1f`-d_9l*6;79n3z+K*720M3J zqC{n_@XGYsgQnB`4s@tmSlPdu78VA|U@M%{7 zg{_H~AbtFm*KZ3AEyu(VX{_>Z*Ewxb5^1Nu;1*R-6HHO=oN)ULW;#B$!iGMK*+aFI{C34|!LQJi((Cs+f_zyWRg{Bre zuaDH7aYl_U`RAVEEah#r{sQN`xL~*?gq-ndhbj${DL|D*&iC!WNO|41M?A_QHtNHkj+-!=65lKt_I5obeQ`Ysx zPmiCs0UWhQWXmYk<-oi#8+y;9CDAg1y`>}xr=ePrT+WI=ooBoJ%*dnaNqzdDc!INapVp)*RDs+6z6`L$CeNv_o za|mKVw>c9TKb>OCREN(i?z75AV5KJ&;*Q*{x@|$fqR)eNQJaLnrmQ-75neyC!l)v+!7UwYX)qaFc)r{9yKAN#Xs&>zup`*DO$-FSEYk zmvKrBwo+U9Kptx7s3HNBLEtqE?zJR#W&(i;3**Wq;z@x4vfSY+kRKStZwQ0X_yE)O9nvN34NI$C z3ON)Zo8COLDm3B-;T0uXlFI6duy^O?6c?MOeTrIoLOt=qg4LmzzKh(=lw^OS((eF* zzD-pL?Wq+=tmkStjZs$$pC}}EjVe1=IVD1fL+1zGz0D58g`g;Z_ygU&_tzJg#7QQ9 zy6~=UOw>~WP5ZSrYwN%8i^p|YYzu84515PiGG`2^UMs0Clgg51los}vdiROtE zx!(;xz~^f9Z56oWe!mqjyQ$;JV0!=x@wWQlfG_7!tR9J!RN;uV3|-lhy`o`JcDS_h z)<&vg^k$58c_4K;{Uj^hQ6-7VNAY6mj{yj}{h9Ua$m&ygiYSQKTB3w6^jLu1sOT({ z77e(i56B<493Gkw(_fBHBR;Xpt&vf7niy?~33f+yzLCl?#7N#U2!<6^77~K-6zOed z)d+}d#H~udx!ET^#?oVR--xV5C;s@qS^!B(i@Cang)s-8z)H84#fOHfp@?a5IL2Dr zIS@rgo6l(m2{ux9K))`NW+Vcju1iZ_R+AKABbN`FJ@PQ`11i9Duwa0t6t8;9PTugA z$;!URv%Pv3=PTcx8|V3f!k~V$g?O_y1CmoKmKsNc9QgEOU;92b_S=zW@F1h4MBT&bb15uJpYV!kQfMwK!s4pn z-pM!Rc~ns;QaIX;7V3+8C~9m$1&VTrY!&N?PTn+47r&vujDIl4DK28^vV0tE-=7y9 za}(-Vcz2ki+Spf2Fe6RTk)Jm7x)_{M#*+v5nPzZ{e|uT0$WX-*bcDe31O;BwBlmke zv7B>iigHIu$((3*Te<>K6IGG>U_}zGH|o<$@vP-&x$Pt&cY`z|9^jM?+Q95<+aM+) zj9E+Tk_AV@umL5R1eHYN zfmJ6dCVt9HF22I+RXGMVpjClOAx`m2EeiLcAqT`7U9K=JkX+VsK@6*l@E9#={&mrv zp8jQ|DaXgza;POncxkaif^-dvL+(45M9*YiTWX$egNv&!hLK0OB}JAgpiz0z(VO4Ho?A5; z#?%re(GTYWs=Vpe)EpOAVaMk9y3P=<2Q9>Ll>SohE{2Y#CS`yMU0B~>m?FrCir>Avu*Ip zV>w`&;aUbWER?QEhxcLA)!^AX_3ag>>Ouj{=ab3XFaTr#yr`bL0AD zhbca=w1W6*8@a|>2o2-tVXya_#{RGPmU;nAIv6C9WqF)nQn9PAA9cGn*Nd%!rC#P7!2Xd7-(1{EOa=W| zp1n3PX_#W=NON~QI&4_iGTUFLXA@U2DW9G3&iNv`P^NxVE48}osmh$wWisTQzA?<% zl)(|7C-uP2DqXQS@{Uka`VDg*r_=^BSKF&=7H8tq`>X6I2FaZm66SNc2ksDtLNHSWF)2qyBHKv z%$Ka^a3#d67m-WH_Q7h^Uh|2gavw5`zE|sd!hY&7lIe>n02LqvuU)Djk^lva1W|YN zXj@HoTj91a#TVGEGFoil4COit-HJ=U?Cb5Rx!}jFGi~G=`D^~FM`AaJVA@G|ynY22 zWQ=V(zG1T}%v;yZ?B!m!dUaV!gli9`xB(hJGoqE9QCnLXM{@%@F5(}2SC~LGgDyVS z)Xw1PP-7lFD5)Wy!A-LMja*$PZZHEj?zEJ(jQHi3zNWvcz(Y4ia%!)yIfCT(XZ}j` z(C7WDJYaSE7VB1{WygP5b{lXZisJwiQl*Eh?9{IavM-^Gz>3J13VSltoeoB^_Qt#sqiT}9Q7zKSFVki`dmfRfBhVLCrg{9Zx8#{;3j-BOK)!0<|e>Enw^Fc8F6KTI4#~C|;H^q9^LQX{hO39Tsvo-mToWvCPGo{IW#! zaW`6IDyfOY+Dc*S5a?#&*c%H{kvG6YsFEkZAwGPPZzZ=~^`CEPDMVzt&o`J-~4My&L+-p3w)_IDM-|-2~y_ z{*8C$bHzc$HbqJWY0!4<~SJ(LkXIOnf6a z>04r~)5P7a(XvoiRBF9cs3SOaDc23MwyU1m`Tnl4l90B$l4cj60-}R*byDb46t@Xe zK0bj$APK-H%V-iV0jaW*!%f{Y_WE!aZ*JzVcOTvqZGF#I@f{B)%c^yP;x}#bI+>KS zJ!(=~QcB-ML3<4R~ZWi>r zPhh|gZY_(#^9=G25Tq{JVaz!}eP;tb^Ke%M=#XB&z zmo&Q{*hny9a8_Z_1W0)#4KPqFeWko#6*R5dI#hn?E89hPEbHGn&Ne-DwoAY51VM`= zbExG!QCw>mQ%(=~UecOau|4$Y8+4MZrwC!4FMXNG@>kO=f2Cpb*>1vZ0$KKx@T%0s zSjjp%=y-H`QnQndYvW3;<=FLfA#D|LJJdd3<717^W07{7yIUTe%Ct{B1@p~jKv-9j z9p=jYjvm^uiNgdZmgtV^6GNp18_V05v=$IeaK|WbFl}#DIt968Jy)0;lh$gVY;SV% ztBg5)*Jnp`06l-!n?ta5B~3Agi~v^%1cFDtWsQ@duM5>jna`|M(Yucsc??#!T;AxT za>w-B5RU)ACM9l&z5eGh^B&5vmp`wx97R_-Hzt=d}@3~ z!jx)e?l>XbgZsRzm#-XHC%V-wxYV%L!t4a?r2s>!*=RM*e*N&Dw-jL;YWKMVBEvPf#hXN-la#OGjSdV!PSl^%XWQ&1AMht81t zKnolFKBSOQB94eCtt*ZJ^(RUiB@8QO+h_s9lfK(pm>PD24fKm>LwsL|d|fo=QY9}B6G z+)9NnJ+QXBEbtY2`R58M?oW{6Z(vr*D%j><6rfbANS5a z&+ZmBlDjYDcRErtpDL^+8BaTFlK2ObZ`1e~vbJST!d-r0qxDwu!P%Fr-=;nKPhI>I z+e<$>cK7LS=@eS#!gXo3q_aPE|8I>8V+%SQ%DPLYD$f-A%cU2u=u*#@$yt`J1tw41)G)1d6?J9~r42 z0WsvLb1i3s$*{3AO<#6yAFC)9xxKKTOy#cWk1t5A=MY7Jt#0=(s0_enjW*7MVw|Log}wLnx3*BF5-Q~`!P_! zw}3e@EBTD?F*Z*}YaCmboD?E4x^*LJJ?QW)tE z$aMCFQhCa&E#58uEn-lNM`GsPNB7h7J2!rFMNA7yXkK&nH_722`j2>xonjWv81P2q zdX+T(qTkDq3gDqeXmI{$->t?1B)uS)N!~Cm^ZRzv!`0zox0m+!w><>UNRUPydmUlk z&x;!K0&Ulec+au8VYzZK;MB;%@|ndSXFV2Pwr-^Wm|g?=ZVLsjnFgAg3$NNu!d=XO zg5fMOd`u{us_1!C^z)Fs=Z$caY>`!6A8)RzJ+WeC7o)<7A=6^vQE>sy!ZY{&?=$`C z4_t6VdtYCE_x1MXVhM?4o@;8-5lEkl!aRB1c;Yt!6eqi(C^hgHTkh>CmKchhidx@2 z=gGL}E7ZGBFH})l4_k3Aum8(91662lH-o&x7`mRAYblhdrz*rO;z5*6eY@O#^k}o9 zeoXY6Nt>JI=wGaI7N}_rvBvMi`pnrTLvD}Plm<~tTz(Ycvb&O_72T1_E}_%aQW325 z^&~6vT{Lk9-C|_5a*FhJ<&`ueAh?BUSV3&hu$CN9Hs)9o`-KQ`2~o<%q}Q^f`zYkjIZJhKxB3h_wbtzUdnrD{5|MupImk;_GC7I+6{P>`WY6j>e3l0|baSr%> z1Q0~Ii~z|NWTa-czo!lkcxVR~#f*ADb@+YBLmx>Z74bA96=aX-VX>qf&)~Fix+ABr zc>??W<0;hfQN>rve7BSQ|BOC>1Z~Ds>dG4O!y8N3yivfS_=brEq(@BjxCsBf{*E+I z_ZGz*b{6ZBeMa1b!>c@Lnd|n!`#^1b)Ih9FlRl89{+0rDy?DlN6G(d&p!F=(`+5^& z@zd)2?94wxNBu(sy`^ewRAORk+TL0Q$7>!$3GREpX9vL~XNL@l#fJsaROi`yykC6! z$!eLJm0B-urMC=yUt<-acm->rK3bx*hi_X@lu*TQx@r1L_cOzXrenA^qpsIoAyMrK zr1w?FRzcxds=mDK^tbvvdy)aEDBMfI#>Dw_G$o0WBcHGjJJcsZH>=~vm#wW9@ROi0 z%!@|SBl*|JBXV9DgL`rQ(R=AZr3c;^@58llmghRqQwXt>TfFD*c~)BbwpQggcl8w2 z3vxxVb~D#AtJ%1kepdw=<4-Mpzxl#*G$dNU_IKXc?Fx0&B#Am?)eQoz;T;~0PD%apub>9XH2nEiTV z;F2}eu4rI^zf)HB^Gd(Mnt*HW>4Uc2hZ%=g9*a6sGo>lr!Had27=`UVyP4&K{lk0EqN5LYRJ2N}wVNPk)G+gPtYqXW2$%05? zA@V>`vZc>7%8rihcgB+<(+f$^di_b{M^@^q-H9}qz5l#G`t<#tUHB^>X5gdDxVIKj zJiGOZ4bFNN%PhPTTnD7jbdQ`Nq1UKll5o21d`o>@2Cj}C7?|u*drJ;b^+bMG= zH*U>I{rbj2sUKK-y!b^A>j=Tt)a6#z)WIuByK6x^jsGk-PO>nlYgz%{oGokGr>x^9 zb$_+7E8k8MHOf*1?F9@3OxA4u7Pm0{8p-iPfP4#^!V=!$87}`h^X4|yM4ad|JlJe< zBWfuUHF>L{9YZx2ck-F@Jr3e`(*KsEEaw-%D;iNHWUrB$$Ikro8s{-?>-p*oD=fhh zVvMlX&Y6M!w2RB&hU5v0s` zNkF0Z=v-AHt^+66a8LGoMu)o3h+1>+YobOoojP3CCOrDput$M0fJuAh@Vi^;DeZ@~RZk}n53kxC6S6k1!#!KKS zvXQB_@6g+7tz$U3(K_F~Q4BQ)GVY^vNy;re1z&!tGWL$p*8!Mc5Qh8FEA%g=oj~Rw z;sXngvRrs9dH%;tp58H@DxmqNEG1pLymoF+u0WM(tt1nku>5*=?Qt9q1wCKLo$xvmEDUl+yB~&vm7`~5@IBiu8@1Yn1-3X+Wo_WLzl3i zkcQ?GNJB(&n2;F5%Gm`xN7#SfW#!7_=~U5KN0%%YF_LMFOfm|vFae>+m%Pxw3`Lh* z?pjWiuY9ddDs%%B2W#%eY=+EoZ*S<#_&N5->x zbM`4V*;pk~_|B>I1$WaA=ICV4k%v<^9vd-*;urc+`#E;0`xPC!AOiP#k(z%c6kw3N zliJhuXn|xS|@PWQ=iXV9*KAzI`YfAa+$3#A}8ZC z+XJ3gknOT#pCaD!yc)}QT|N9z!DEMNqNctNh+j@+Vkq+ED)stcQprbf+D* z_1D7~G<^mjL__AR#FkG1=t z%jTV}BH^^CocQlj6W0B~~Pg{_tOXs1saKsH$YeMrArb9Bo zxw^v!4Yi4CGSX`u3wqkH{8f3L%h#5wc-^1geS&~$`cxj+0ebs^*PS&Y3}@$aG{hB) zndl&HeFilp6?7txQ(?CBu_%lwTTQouoi8f9&sCWmc$`t@_vDuVKZ|T!V17p5=dqCs z6!*&ZPpNoA=!j;VF)1(H($UPBz5#(eh_~j?PLJ>LLvk602yC4sqK!4t@gl0l)ndxU8 zjF-Pk=U0G@YLh$o;3j;YVZ0>oxQczk_NcAsq!3D(g;rqbiHbfD@jQeZCpVq;H2ZqX zLi#|pWrc@x=90I4*?p16$wEEb-o?@5lf!fHN@V=wGhH7Fs|r2d92&`;)(k)X9&iT@ z?tDWs-a-Ul6kC{{`+-MI%~j?~<|SCwJLovLVE2pck>1aM!Sv&BvtZcD`;eY}fh*0R z47h7j?yOz@0PY^HcqQJg!&0-?Ek7cJIwF@KWL=dn*&Pr>HX_4>V$-P4m}wUeQ1ToY zG+B~V{g4wqSyfQ4xiUBvm+!VB43Ry1H|EcTRYZ;=vP!=i)>M~w`Bgv_^^>DMD9L0LqrAt!2M#q4 zX8kd-s3xCx(|7S8RhjOkw&c5a397m;o3$dVKY4#_^S+IeT$*RI%sggn8J%xK;0Wn% zka+=FLd9R;k&nM2T7JolwNOU4N2`g(2;y4D%p|m_Bn^DNndig4<)*codAk7#CC*f` zU_2PTwd}aw*?;M48%~H)M8G8^9!z44e(4!QG|>-1P@C9D_O3<*`V7&kv&1*9pjko| zkK$8+_p7;v?iU&tdV@RGQ%OGb!Q@+%n83M`Lv`P8!+w{MvNtLRGItuOGk&Pg>GASj zS*r!L`)W3?_N&}Y*S87souopHLwWhut9f&GtbjYjD`55knmFz0;@p?z{QjJ*c12Br z`9O?Wa*w|K8T#G6uTK=s7S2aIV<-F@-Rv!J7Xt^UX&coox+>3AU^c1A*-DFeKUN&m zRKKO?e3&(9>A&B%+%8F+b#%fd2*y2Ch;=}rNhg$N+M(z^`}78h92 zxfW-4$NJfl?$wg0J3pl-c*X`lRbFd<^Dg>SwL5EIBJ#nKi)nacQB+2{_^D|kwxji{c62Ow>$2#Tuc|St#YGzl(=LJ}QH+_J`Ai zq|zkzrE52)Gh;s-wJP)>-+ge-RsWY9|8H+*K>+kPs)bleT&@iDo=q0yUtF!i=VTFr zCRS;&=`x*MhfY@SBC);E|@nl!n_otcQ%^)2It~fyu*Z7z@1@0IbG^`1_ zvw}*W%zWYnUc64mDZ@{i)y7)kK(w4vQan*3dN3=Nm3(ik0>hYB zuh5T@5y?yT>xsD)eum097m-iu^`Sv#uPqI0H- zx!?40roQKE?WKdwNG)4GR!aDO3wy?z@H%*R-}PkjoDGsqxev!&I!NDzvj-x=13PN{ zYz9}c&&}4_7t-mG#|L-8{wuj){O-$(u+5-g>h;CcBZsR$&8(bTF?LetqBQy8_Bb>J9KwD)=;l8cERRNWVg@2edTL~CXA)@!`nXunTz?#E!Q*0NP7elbT1gtzH~!*j42RoeV@CVV<-e8Wv-YXeJDW_wRQ0&E!47^BG`?U}c}P z^fSltyOw{*ZiLADU0FN5x5_q0M?^gRJY(g&68q@a+1^JRIb8caqyl&0qTr#jO}9|0 z%Y^#Wun^TC>rCL(F^k@qGd<6FDXkq(WN#sT-uc_*?>k441M9duqXGo2_Um%*W{WRH zhAovVQGuQ8q{A~vGhXKHv5h|E1FXr*C`t@pW4?wBmptQ`_+I(RYUQbS$7RI! zb~d{p8exg8(TJgF$-a-kl1L8`37>;ju6O+<=qw3=+D5wgLcjj0^a&cPA?mu}UqBG; zV?;hw>A=Y!Y2-)yv4>_-b7pD_e{2Sx zZc7JI`I5QN{{4!19zPH?N_Br&?SP$3MI*v%<-=)dko;e~z4#YZ?^Nt#JXlFVn$35D z*Lg*A0QnND>O$1vFCJq)KA;>Fd-Uy=F*ZEF%4XEf5!2AlQxqg9AGct=lErn@l`fx- z@tm9s3?Z|vL1QksBu59e2!w5|sj_VfrZeQC_$%zBLssfNafaOGcRccQEVEPM7;mXm zHc-LQeQ%lAJ7ua3<>D{+gUuQ1D1(IIL?k>vFMoJ=uru?;sx1*=JO9k1(nOEM<; zaulnnzQp0er=6MH@MGkZKIUckYxp}yje(XV zOMHO|L0U7_tQ6i*#qS@jzrDwkY9#Ns=cfjc?JqF|J9a%Sq&`FBX|bmnurCRX+)o&; zD!>TtIb!mp-QAE(ls|q}WWE zH%n>?nv)eC%W;@{Cw5g#mi4Y0(seeHgv$K{YavZ4GBKlNW6NzmhkS*w()Z0)jP2@X zTJH1f^Mv^%-8iz8Cb2zHr)SMLd1oA$5ZA?&b`v7aVA}SE|$^6$R)eGi>_L1}$nQGC^TMxre`ETb3<*8Q5{|hYbn|~Ty8r*aAJKDiU0Q1L z=L5fqUQ5{)(@3n_h6iZUO;+^s0MK1sVfDV3L<^tNx?svdlh$8u3eUGc4g~%+#gNmN zlWLJv5^s6ZOmL4IPQmkeohW<(rDX=_$j8@H@V&PGJe}aV0xKe$=nv9GafSR5xsvME z;P`wRJgC5!`kl{?ju$Iwchf$xE7r_(<9HZNNQ ztbYGd*ZiAg!h(<+)10N1eAT*NA0XBOrq&zAn%$n9u;Ny#Z=)}uTOwgZ%!F?D_4T{{6&uRaCn)KC6@n54~_MZNLx_%^eAdAdk2y}e}Z z25*8m+>w2yKD9<7MEYUdlG4%(eWGUqWixNbbmbqK@UFjASV;kXL(1iRDlA$~qwi&> zc>jWQy^0U|F{r~glWSTxPzwCHSqCWpY7ItKYHfy_zKk1nBETm}!e^nY0WmpR0`>9I z48c&Tjl}FgS|fRu>^p@BAlCP7oaLo6yz!Ym%iX<_s|Ek$qp+r5-WxaNOk{L$69%dT`Jt}Kh*^JySSz^`jUq!Q2F5TeM*cL^4?*AWRiWuD z46h^Jc&>PO?xv=SfxHwFyd)aLO7Z4WeMi^bG#AqG6*De)ge6ESl;M%EM z+Lo@N$h#9Jq#u-17v<`8+ygI%mQwAl6axc|5Vsu(HM`Nn^0)^mpqNU<6s>^k^(IT& ztb>!>5AgTUKzbJo&B}x4-&i7o0u2sIG+mbnp#lesa}%sgyzw3&*t;ldHg90re$u-C zg?XQABb_BP@Y}!0)&Gmv!cAtE=N4U%J7{NZMTP#wVqf6?eKv&0Cr@!$>tvAHqO3^F#>rF0x_ejdxg)M##sQ5(VT_OTNs4)bD0ljzNRgxkdhR|yTTtj= z0Fw`1_pDDa>VC^cbmR6q|B3QVCg&d?<}~}*x}U`;9GR^bzcXePeBk$x@8V`1zL(59 z%NtJLl{@FNAb3;gsn6t5q64UJnIQ z3C3kUxw$6}S~_tlJ)^Q+MiY6m>jeruZ5gQD$+z{CV~vYmJ?Hs^D=oy6G}9;TqqKq= z^Dyo24<8B5e*fWDU)0W1V70BtCsh4MPG z9q>}nDG~^&J|lk!clAhbQ6lhOq69^~41Ae(TXyl7%L|+D@ewu70UU z{9pBnJvJ7$ZWdH5W?GI)W1QNs-l(iGc}%0?4QcSiKaZ>eULGgFUcypPQ!< z1K-dr$+e!Hop5h8u7GhqpZ_Jm+s3SAIN0jwjQBBrW3|4H$ zf;9JyU=xM+VFF{62QV>9V`yGQCtYKCSED>;0^qz9YPihBbJ|l>GvOc9;kKn~FAZ3S zTPzZ#kx~+YCV$>=*!aPzAW;zLal>C?^ykJ6z_Wy6HAcOg#5KgzXT5}jN%gbxE_p)7 z0bMZZ5=B6YcxjW{HX`-Gkl+&szT~T2`7rX~%DODUN83G`&{A(#9N(Zw@8t4q?r0SNy8|~D z-NP&emB5P*(ZK?Gou-kQq?}E3>tkN%__?o@NkA68#y zf@~U1v1k%Oh-=7qUJFF&KQXP+I5Ck_6K?)bjd~UXoK@dHKs1rm$%Mg~2;3<3YUIHu z4C;B+Q{MMo7(k4f!=CyhPI1B-Jb*MZ2JM1>G(u!dncc`C(c%NOlbS4{=*llAUK5KS zE~GfHq@INR<*NOh5B98eBUSb58z-nL-Mz{jksqZpOy$0Wp%z}^08|kXGOLCx(JTWP zsJEL|wGjGR6q9ZVe6}qoAh;50F;}SUJ32m2mBM!z(Jr)`39-0F4%1UqQcYZI9OF9?>d~HA%(zfBLC|vqi0z zH7sfLmINW>7y&T0!~$(`f1FHVY`QE^yg!F@d?ndKT%FO-UYwd|< zZH930PqUSg;~xPWWe@elAd&B!-q4v}bR5l^2zSa55`JcOU0V-x09|O^Mb|k7z-;89 zrNy;pyl|(pyaciE^Ly0WmqXnSuOVBCk26`q@bwGtYf}sq#tXsr>YrwtmGO3wSe`~R z*AYPuATOG{=yH4`4VtuH)vn>b(&gw9%}oy5^w}Y5S~P>q(rlrEJS$iMsU@G$MF$4& zlJznzj`9>|pgoZAtvbx_Rw+uN8MMgR`Ip#;w03|TSCf=s`JF!%x_i1Gly{UTijX7z ziQEFqvT#YFjlYaAmXToae%Wz}Nbk+Mqa|TyIgdyPb!}xGzrAO6TQ6Mb;zn2$z@){` z%c9H`Al87&^&H8CAFRe2qeW0+FW)co7C>@Q}u*X(QHVzAYaNp&wuh39GcC?MkT(uU;1D zX7_3m{N&_<@?lr*nKBQErF$So?h2&HMjf*(c^;F+Ti^3(GfuNp{(TwoP3cN|tW)&4 zHQdeA{BXZpT^K{s>n3czF}tGEug69<=5TNJOEFJ0jBH4Zcj>)*8~LDjGRUInjO2gG z3&ca7Z?R4-p>4J2X!oSgbH5#ZQ4;3kp51;_jq@%*c;9B463I^Qk&Zhpy^pS3KmP8w zDNq>nwyF2d%21BQTpbm)U9amFZrkmi@BCtwQ?uv!vC~ct5~Ux8uJ+JWlR#Q1@qFCe zM7d8gyk4-|-a2L@xz@^9u0@I&WuG4pEWpj{Wa#%yIHR5khB$rL_G8k^N72ZM(GprQo7`RC8`|Wi2#2(3-BuGYG-V?x@YlG^ZaewzfNggp1pE3r<}pIvM}TLvJ7za98ETgOI2NU zQsd+c1J$p9*em-6w$!m(x$ZEr8&5bLuAy99oRnaIc#*$mf6?k|xYUQQ$-*<*+bap- z7OtM|ky2aEj*bylMiEjXU%Ah>nw+iL&5w6IN^rQXc46HvJi3pa(pIhn4UEmt1cuKi z8x$ZaaZSTg0<#T0yKU@Ss#i5esxs~bd_Ah&_u}X7M!r>n*+A>4KsR^2>urj;QyGuf zAI`IB$0`b2eHf#zRu|2lsCSA~icv!PtEZ0C#v3*- z_c8K+wREw0jI%@gawn4k?Uq_>4^-J6JRiF@Xko=il+GTG(7En?CuNtLDUB20X*Cs? z5-GoS^y=mRTuQO3I~;=^hKU=A8B%2<>wx`2^l3+8U zMsd)&vq%1OjT26KXAb&LyaC(iKlu8!l2-dLyC%pcGEqY*yVTe>EA#X}&tN^zA+9av94J9C@bK-`F#p`hEX1dPUriO*%tgOzw<2Gi zeE0856(U+`^9KCnf;C%l#`=bJK_9@iR_&vfdib@3zu#DKWe}krKCpOJ;$g(V5PMaq zY-ZCR>Hz_=wZ78cIh5x`H~wP*mNk^pej7nCS6{^GWD276u9P!`WMaRHcq%(BoUL)A z=ON3e|Gu&ocou`QH3}3Hr^k|L{47m6?VRG-z(W_4- z|Mr^Zkl(NQ-thLpi+KDuxL6r_#bNq^j-gJc)=yZD#>@Zbdz7==p<`Vmoady>1_zX- z%uEVYXyaVK&J9_AQ}sf&_n-dLELQun!p|>SX2+}3zkE%8v9K&Iywe@Yzy0*u=l;E= zQ5kNq-QVoKIKkEcSA= zpEybu_4Zr76c#a{MvN33nLt)UTGH)ib?lDz?9Y@rFVpPU&V#PyoBx-=RN3I-A*&Za zUmr`qhJSpVEy%p_da%D5IZYcrG(XL1nfkN8+es-?b>~$29gm}(^?VU^&2=w;n`SLT z-O6~rh#wK6Abtkiapr%2^6~d1`ty39T8iC#OSJJTL|5#Mpz#=4t4B(Z4(OQo@}<}t zy#FGlswnXIL?6~rY%_nM5%hbQ> z%bl$eOzGsSvW%gd#|mZtK0+Va8jKf5c&;sK%+Q=n%Qj7Wv8By?=2D(`m*~3~W zcQJ1w=Sqim0Okx`d-2{MS|b1LBd!i=oV+LQ4T4w4Gg)g}>rGq+>twRlDzmqL>(o8f ztn$I^x)&LpV$SybUb`ms{@L+d^UD$MHZ0IR=RxSMB8$II3!#t(^}?aKXCs$2bGXzp z3i~gBqKFS9vxnKlKfeF({f{F|xHhLj8I=`Z@7qd=TM za*2YX-kX%`>5Ja$J>9y0d#8`uQA73cnYaFBj08q9T0WRVkX_^9|32%w-gR#f=+B2- zTXAv<;KkYap$Krd;nVoP^_Q-9R3?n;0cU(b@gI{}04v$H7I=M4;$SiR6IOKpx0Rp% zBrAH*W9AxH!f5ZK@H26|4~i75I0XNAdj7ws&XIGA09Na89PzTD$&9LK`82&pJyS>8 z%+*EPb0GiIMz_nAi&ENOeq0It^6s2)&Cs)<8Hi&baABV0^iJTNAcx>l{l8D+tAlfF z#FOdUk4mrq^Yt#{EuM6PqL(NB6t|omq1q^TW}Nt# z`6h-vxB71uc&D0t{s!3Fr`O#gcYL}CT>9siFht+YH!R=zCiV?i_TN_=t9zbDH|*F# zS032gN_yYE8WuSq&vObm>KH#d5%WFn4Y*S+^M4FSR%ID3TXlN8UhgK@%fvponddUm zL^Au|E_|?_%?vvH3xtlI!sE|VC$fRvq(RYd!;34LjZ%d^XzIG!%(!60=W)9hJJ(A0 zJTj@WFU=-tZn863A4XF|vobK(vObot_{?&jbBCtN`C#tA493VW`eCKcGNTz4ummL&1Dsj2;#c-j zJ}JK2iHWQ#?a=~@Nzv?Ma!>x*xAaFpXxJ$(_m;iC{FckGm)=K2*G`GwuRd1*nfQaT z-?QS3mX67Uew3i$zmp0Or2+g&!y8RyQo}<2#2ApiQbsWl;e<|RVC}Vmvxt2<$1!%U z(+A8Q+F*uBCX+@BHzU>bQ(Qx-BDAaqe7(%dzN+3C z{M$o;W3=K-tXl9Tsj~JH#YsTgNBhwUQN;NC!W_w@oHtj=s&n1nRB@ZBtOD75osP{0 zZq*`T^TjHpk2OGf+=@+D3!>Cbo-{bcug{-NxpMaE=Dj24rU6dLW7oZ%UOlb($81rh zuvJckwVAdCAxyAt7JMLY2fo|SRojpJoNtM}BiqNv!uee4BV|Q>{U-03*|RSVSa}BB z-5Xbqk-BfK4~PtMv>X^k_TD*I3RkoUcA_v5K%0GmgU5D(&l+7xm@w9GHoGEN3R z1+!bRG>3~b^Ab#d*a2oewa}7v!r1Pk@j8F#cM~HEr&Q1HQhU8Xey6Ck?LBz3r^_}% zMm<^f{tP?+wbGF7z9HK=>u-|)Wu4JU>q8-pl}*|@<3#D*@MArXgCvWF-^vqH#ybUQ zEjo>HO+BS0as0VwZl*&Y96JWw zyLVmZeZ_g5pU-)IM9!iB^ig7O-zGlxHlrVbJc|az$~52OpKl1U0log2p35JIA3_5# zf1ACmh)3>o%pUEw>#~XtlW?#Fv#fJ|n z*OGlzOr1&>;PG+xCr~^7@OF_?Hsua)UvUvmz~PHzd)<_3n@n%NsrRe5I68|gdxO9| zunlWdiK7y!bmZRASd+)b?puc5sv)R>QqxFypU`G#YVLucOK~m30U@Y zy*3qSzjB3p5xYN;2Vb%P%44J3pM2 zn3$6Ny*{sIf9SpWzWTZK9Z{{RDaIR9U!t|Ny!Cb5ueEmtGgrl+z={SfJZfJ z+p zvPj@W1%d1hu}v)%`YeDJKhYQ_9`aGAzg?0dE4(d|cYpCDZT{P58xS{0iYY$H=kcoc zuD7_)f49ki^=peii0ZD|ZcmpFiT4{4yAgVt*sS%({XPw`Gz2>q&Sljc2a9$hTJ}_E z1)PEoSg1wIk^5`D+ha8qGP@~GWpx{CBt?S&$3rxe^Y9&u%smLgWNigEI%>AuWSQPu zY*#6k$8l%HX7aVj;gD*913g!dn?3ip2>VwnZJ`%_tfNun^#tJXmL}=Qc56|GNo|c6 z^_3>a>%&F8)oZ}DDltBPN)6@D(3t~zA(k7cGP0#Q%1DY_2s0fCOh~f%!5uCMlynXe z_lXs^TMauFO5%RcglQ{w5gVx<$%lSZc3T75g53<{I#v+hdwCsh zw2Bo&(>8C*=gxqV+_diB#w8j{k?u6+!0kK{%$cX?t|==Q`j15h^-H0L}~iG z0RldQwgYY%78C42KILy8hJc0L$kG_iCV2kxNHj98n8=qo%v7YAAQ!8y75d zBMH(i7d{<9WRff-P4UdKbyq8rL@|z)@F^46G^32yaK>lh6Y{sCdGevxzLhhJd{?Iz z`R+VjW}AXrb*?F-0gufGc9gY)J2TSf=!^R;rd)Twl_*d!Ga}ArZ#aWqHVsp_jn3ja zMniX|3g_FV)}N7T;1)=HXNYVhftd1PDd&U{cDA*@cSzM+ZD?_b?4@s_gu9)V-w@;F zUa0I%sz;SJkex63;>gUv4t$*4GyE> z5^>KCL;$CXoXIN@xbiyP8SF)-AqKHP8y>y;Oy0kOLtZPLLfl2aZT&d6)mym8JAXUe zxez=cx?5%L#9=7Fl)^DNTgb%Xk<&kL+01+=sIiqt5lu0ng)6XvQSxchH`Ly2A(i{2Bj0}9%qaY@R+#5v{LM{ zLGFLU-{?YVl_U#YTuAXarFf|_fiFtt6FQ6IeQkzx2k+&Dh3C#m4VSdvxr6iuds0ho5CP ztejGcM+8j<=rCoLsV9i(uqd*$`NQ?q&OjDMdB7}-Al{(|2}X|3@b&S;W3*aAwCM`%*b2GZ`$?(8R1y*40hCgEZOJQ@ zISf9t@u7gOwNwVKp_Js^zVdq3_`o7j_WkSQwx#C1XEtSN$h~5FhOq<4wUVa7W?iy0 z%(Z;ezenLWIlT;|nU?>cKyOg2x{UaIMTpU~Mhjlc88%4TVbT8a>Jmcs(rXm^yP!1e zE=k5{dk?_f;etJR?1}Sy(_Uo;uE;a$Do}2A;Q~_|{GjVvI^99-Xci#*CWJs5Jt(w< zPh8{K-j}-B^D-km0Pz%1a7 zPMi_6;`qdRQeHc|I)p~1DwizBb>HB&+G)+1E-b8f(|3E&FE8vPe%4c2GapeTcb}(c zG~IS;+3ME!OGr!f1-E~@atR0MTRAd6fYV+gjYw0zz}_+)BC@5sIv(^!M+&&6&{C^O zE?$nTlo}RK-u*)Oa^|IZLR|fvQnRq{ZpcPC?mL>co7uw;MI|wFuA6xLF3f^JZ?*Le=vW|fIb_;jISbY!I z&Xd0~%vd`ytU0W`O&MwwG@Mey7@QHNA`?gOL55&0ov@JtMyA5&$R|B7bpPHD$q25o zG?_FMs)5*OT|Re)BG~V-UDr@oml+{ly7H7LATWqr_8F-kH)td(@zYSfu0nbU8r@s*z}wew4)xf&+==1z!IpjuVHwE)0+ z*ff<;onmyyr`p|ilA3p=cM{NT%)>Hg+XZ!=Xl8t&Ef%`6K-o4{evUSn#6iR`*RbEi zg$O;5XL015wM}tD_lv)9=B+o1#B7P}ubp}Sa zGCL3J1d zeGO#LrGXjHK?Eh`4{xhh%6_suZOji162Dnnez8B-8M)T?QPN^jkwi?VqX?3emUi7r zrBaq}FuB}DY$9kTlCx(A{LYb?5^&p0G0+|jP7S|o9$cSerWX+|MvVGCn)MqtDxns5-SlD&5l&p*RSumZ%ZK90< za(cjBQVUW-o28E6rB57D+{EO|2in(Ax1U9XMM?w(soyp?F*9>~u7Bey`DVn5ddWMV z1u#~JOzOG_WF?43jk_x|`bK~da9!nQ`S|rTl=!IlMp5oE?6_U^{$|VI1;*`D!ni8W z;Mp=Yyw2uhkc`ayv@~gukHh%I^sdQ`8zrWUs?+{QCrNop?mHRbG*RI)je7bA=j#QL zf~l;Oi^fkNriSQo&r-UmWMU41D6dIJmm&T~>td?>Kngl-9%V;eP3!Uq5r*vp0h=oS~qhuG1A72Kg{%tG73p^5%aO=GN8AlK&@#`6* zgPoSqskhfd>^}l5J$v5yL!Wkbs3xeCkEc|7_{<(~`}SS2PpW{J84cVus%<9Ycv1B# z8Lk~}Q&vRUvH2Bi#`;EEy&2o5@q*b*O!;?judY>UpssMmI1hsPLmd$mUN8;PPH!{{&OnM zxkOFUTt$q7j#0`h|8p$-={p`V`-R8Y8{sRJQu8EC>nZ$c)J}EdYFAi|nKA^P`p+~u zH8{BW3e6OGCDpa@zN4Hw9p)x!u_q)UgZN7wr@as9P4}Es1H~&7$eX(^H>s#tgMRh@VB zmy%zc!R@o0TRjYKbL#n{gI}9>cy=EmYViKAoJ}d|3 z^4uo7O+F8>Aj(xLYj?Ej8(nu+&SBfHtrQ?p|I7{YB-F8f1z11q*1re}!HE@T@2xQR zchPQ-3k&rOe+k^cG?{o$C8=8v?D=oFr=;awH(NTh=bb%Q%Mcey6|ncc{a6!utUv5L z|A1!sCp{kE5OHvqX1~ca#Xrv&9EqJ=@XSI=P7sg8C`VlDM7?@qE;RdW=o>gR=*UCf zWm(*xZyW!hUN)`Wa16Z6>yzxwKRe zJg^<#pjY7sn$myuq^!lq`8-rV0=Qq}#ei--smNc_m5e#rY4!}^R-cmXG=cj5nn~Kr zk>i;%w}?2WORaw|h9Szy=1{X>1zV(%bsz0lg^S!Yit||uJYkzj{<$qFHsA7otlUS* z`VmUEN{hb(zwg_-?W#JdCMA%yuQDP~qD)wYNO-irX1z4;?!%G|^7lNHSgGUJ!moxy zdzz@kMMch1xg-<(oZODNnj_kBY@3Wj`9I>f#HiQte{YFVa(Xg3fiyJf4t4C|*|JN> zPa|1nS=^bKd%5!TfZCQNVv@T=A3s7!yaEDVXukaxM%VLm#JqX1M&HaNlUL1F%)>5F zbkLk7l?eI`cJwX_J!6cV&UUp6z?G>xSVpxRf&yjU$>O@om$W!R>&7QVX;Rwr4e_V= zDYLtG3tlFB+Nh_BtZI%UTUo3rAO#Es^$aDbhbyF97o>7gYPQ+6O@z*bNs;wgat`nmUp zKk}(UCvO6Pss)HYM$l8%^s=Uru|SHc@Lun^=dM5Y8qG(J?7kE`dCCn+{h!~P@AmJ_rrTr*zBb@_EJ{gcNa9pS$6 zeR(dD%IMzhCkR&>o9E0aKy#)oTiMO+aDwfX3N%-*5%e&EB5bw3yx@^94Z5NrNR8o6 z_21xA)vHXrPf}Ok%WH4NFVU7yp^mQ+;R~VgJ%2#SMOjWbs_pTbC?JTK;I#*w(K!sA z{ZL|Bx!4HW-*p43@Eh4Y2Z}|ctRM1*r@-8btCtVHMda$|Uq%%125w548NHYF%?u6?%@2s`Be8a&@Kg#oVEih*Ndx$)iR`7)uRVPfR0s!aebwthWxJ zO|4c>?G?zy38Y_hjd(d9Z(37wy;n>>RZFB*ta@2LrkS2U-2J3=@tMuDT_2^~M=g)P zd7|;a5kYpr->aS_al=dG_fk(a8wG-nak`eY;_Z8ZU@Q61yM(TLnSKvBxiepz%RL<( zHG-DqhiY^P$$vu8SXhFo-1I5c)6eziS!=ONRb;s5cn0^`vNg3Uj*QBwz7zQ3Q$|T| zFI>~A2Rzs_b&44mDEq}hfa;NGN!w5VV8>xSL+6^A zUIZyPwg0%RFK}87_5c7+(}- zZK>e~hO#$>>VOgsO+mbsyr0wnVQb{wK05YzA}cyr>e%1YbQ!O5TLbys!x&x54mvNk zw-)PE1M#I)n%=3^?lNy)QNmJ&C$uc7ISFDeu{)ie{M!uQPfSmYBpfs^$F5@1gp{}E zL#~orGvg>yI$M@b;U;}f{FPBP_I6>p9(6r*#@JM8XRZ1p=lHn`kBfXCW%l=5&7Lv^ z2R<=x#+UY=h1Pakt>~6WA~a>O7+wN-Qhtt+Fg36p%SPPD#x>!SzX=UNC3Wt_D$l;> zjT=>QGw{c(v!}sY@!D7Rbou?|2_>(wt(y3{sO9T-%s$BT z%nlUk>EX2%6ZohfKp)T-&>XE`PP!E6K2h1dmEU^!gTK4B=cI1&w=!;*z`2CDXsgm- zwB^rS1FCn=J>kq<`Zo8C!%<#JHtV@H%E+oWSJgmeWETaF@%!;l9A3p#w6@z=&F@r= zs991}8ugczx19CP6u*u|O%S9w&VF^DokG|ij_Am$|e)HKvA>zG8K9A;grXOkCM_T zrgeL&j46WXDp8XEQv45$uZl-uM)Z(i(3EH-nP+=Elz^vg>!AKjsazJWuX$Uy^g(!6 zJzgo%8?tG^`UwbiCuM}UXqvh4rEeJM@$gvyO(NGEy*hRe{FA6JHs+`$gc;&33vG8K z?}4PqCl)pT-+q%BoAj>_&WSfuKI-S- zZGa6w(J_gWbY!+#RLGoLTSsh%b952|sPg+`u=(MT)^w!74k{n*bT4PF^8 zpp_^)n@J1p+V!hHSa&11qItBS!t-NgI|<8*qO z`GY-&5eLeyl)gOxz7OiJ&(rxk9Ku5QX5ZWP8i~X4mb7oQZQM*dWiD+W`%5;b zvQc&C-GhOTycVh{BE{=uSBXiszp&A=F}fHA1$o^y&xxvS&!Wuk=8DoRj3y)jhAe(~ zW6&X0E_1K71>+!Y^XX}m47{?tsh!l(Z$43I>Ujt=5m3L}Leas-$YwRM7q4DPjlqY# zD8S}O*IZF{Q2&KQYL)7YbJ-k5xDGQmc%e-7_^|SvTe3WU zk5*BED^Fb#!$?!vq%iPJJz5U>pl(mN=gJR~>5+u{4hOC&%%W#yohgI;J^W~_VBwug(~(W&vt!JpSecLdEVjofdZyYAg$m=bZ>RQxTR;nmFTqtde0S>5>n3}gzI@& zSak*b^2_y~o|=RfcoE9k1A0CDV>jHBmM~t%1TEb0rNddhpqKL0pcf#7{@lW2^w4sh zx4+LY$Wh{kug~F{d0sA?WsG$$#@4}k~Nl<ac#5GQHjOk#^%ws&V{>Jl8^69YL6$rv%wobs$=5>Y)m@$29s#X>0@Q$1- z4cA*nwve0%>6()e~-2c*fjf^`NJm!e+pN`G!n6yC^84wAI(pbKx+ndAHbJV)Vh8 z>g$xh_Wd?|`{iwBDy+;8k)z#00+O9gPeR{Bw~hUHwSw~W*)CVssx5J>j9e*9G3*q+ zkr1rxf8yUTg=i_vaiP_cGpcs2~wlR zWvb{LPo7=R7!>yF;e+*jC>f_i?-y)4e%aKM@(FEnps2SO&~xq<7sRRzz#e|?P|lCg z`ShW}Ddn2i9oK`CA5D)Do-Kk9eCf6G|>UO*%)WmHq zj#vFzX_Rg{k>MC+2WM}&NV=<#rKxr2oBfNzp?bxiVZO>%OKLJn%r#qDJ6T$Pc4md! z1j4yD3xsqd-1d@p35Y60ZG;;v`o4*7M^)>g#M4TBfwsrpm-$UgMWgf;GyyD%X6lkd zCm8qf&h$>Bj?4?*EOkG|s+{%lw3U}g5SL@*{l74~5q3hO8i%k2{|dXw0?z`WcN&KP+LeU&$z9$FA4iqNxkKguv`DyY@P9N{!t>7_{?rNjXbNdW(2*2~;Wb20M<+y~> zo@2}4=m2b5?@=fv_1k1@sZLbSPA5ETrWa`K9v*po?CM+ZFjyta(&W%&=qP1{#!c=U z9T@E`s~C&3RCjQqmgogOf{(rJQeekk)zwEpd$^HpKwtf7#{=^N6FmM$z}vie!mROS zB>$W*?{A)ISuC~Z8LF1ZNxGwR)qbjoOl*ch*lt2i!bsKLhW!Jg@!|s88ak9B+H2Fa z*RXA^4y>ac7dH=2+jJ;1nx1lYBEP(7`|N`CFB;51^`*ba;&qdO$HWz$jfM^hs685A z#f`~Kz_<9(S*FDTS{WFT(HZ8~nqQXgYvYTa!59{9=C{Nn- zhd_AZ-k|M_g4h$h(S^bpgLImPorgrB!DyLk=V|AT%*PxU@C+TwtZK@{BF ze;;|a5Aj2a!pXeTsFKaor9W`Dv|YW`57SK*UqPDrD241?xs@kwrkppjQcbkwZbHMIY64T6mfm70@Q@mk`* zB`IH7lNV7o4SS7(4%21Fk{_Ax<|fq^|3G(EN;uCH{uE7`o@u%t)LQDxalVfaR*I^Z zR4%A)l?~meGFS7Hc~^R`wcJV^CtYl18a7t5YWd@$4CCn7{D@a33cJD#W`=b{;c1fO zR^OE@-o~RK2`k4i&+0~}W#}{9VW+N5r7iLnJRTr5b22P{MCa^@%9d z@0oL_hf`g1bL}Iz@i-K5$Uk7)tAEt<7T{JC9)?O~+O(}747nKG2yCdu?oX4#wV#=( zRsHZcZ~$;rD;w90LZQu{ESOiV{I+cX%WVMDYuDTH;e2LPTAWKo>S<3`}^*(yB?(pdj+}8)zD^`ZJJoY$dJGm(OfwGZi!U*JwZrKj|t#2!* z;m5@gJZIaWZNAp)ayrIeZk3HalzhwUeXcc?n91s*=x3jo@s$e|FD1F!{3Z-A;;?c3 zd0MLsyOXgHTuYQ-ubJe*ga}F@&96#vSbM5h1m@XV6)j>rEMS&#ntL=s=MF22c3*v- z8d_SJYM|}#d+fIq>xx%f`IeCR@c?Ba6pdhP>MLIOKF=p5Apk&^aBg7Mxv;HJ1Kg;Z; z`ZBU1knsA(AEaJ_TWkbmEMNaY48A*Jd2Qmh#IZL)OL!xVF8tsC2E}c)O0vHNvWqzA z14JCA>3^!sL0Y7WOo77{wEyYBUvH*={Qj`tos;(kI%FA~!C&zF%7XvRuYC6$3F^3c z44cwjk9EXMZPtk_eUUn8Lx@pSTBiqKDc4=Nwn7t$HYbN$F^_{%fWB;aVC$PpysZISw{HYp6$_v@QFSent~VGGr4aWK4235gB~rX z&OsGSfe-mWkl$~7_k01@3|%zOW`S$iF#O~%1*dVkXEeSDRx#x7QP4Qs$F-z@qY09N(Pp)d+{3tyAyk91GVUTxJo4;V_0X3^)rKUhA~S3`TFp8~dH z#8~?!MhULo|6^VwS%?dQqD%$VojhM_DYSVe6bPcwV%^2|Q~;}v;N_sF+^^q-d%zx( z!1}k}4aiKegQnm@5?c6VfJKczXw(6$s(+D&U+BFll&*eMnBvS1n%?cc4w?a-8$ zrEb84FVv643b2C?1qr`u|4EaUPLZ1wl8|a{|EgskJ7sSk!16WPYVwlu%9YDYd|VOw zZ=Iv<=BGst-R@YpZ~cN+U+4-q!_&JlqSr9o8C3eet_9eT5mE|14Y^6C{~0fM9j79< z_~ITF)dM2@1q9}gl0K+}ut~kR$B{a{!1Z*QYwLlCx5zK8Me6zO7BoCzVZR|9HU}P- z&Q;IY@qYof--Q+G79SPDs_l)zb zT5j~L-mR2>KsMh;F*ER-OJL%5kbWx(H!;_}0lthkC%gEJ>K#|Yv_~^*Jb$#xBdP83 z!t}DD4nybfx<6xj5;x#BS!<@?1FQyOCxeGvVXBl~aAiuHYVsM|vA9fTI1TZ820ZL6P!OI?sn5$xO@2JdydG6`=F8AHrg@ z1!GS77WzLcy|?Y$(w7_&FW5ryLSRPFDcf99MV#;VK)KLj`_{PD29b`~ia(9^fKcxa zvdYYwaC|^U9MCcgBQjSrnJ~hdY1nv=l*`Sm~qifP*k0w;GsV6=5OSy%B>U zZt8#h84)2zmTt0<*_X7C93re@Ga?U_J7>^oT~1){j%0HU^r6L zq_xgvspDiLOP}!%DQR|>w}h9_){6zap;dVA{2>mFmt0Q9ccyqJou2L=A7UoNU$4OH z{(sm1pM8E$*Z&(2e@)i^&+`SvJazI*-LGO%Yy7v$b~i!>TW<(@*t^?=9$wDRO*fRn zmk4?VSJ=UmjGH=0wmXN%)k>QV`j_#E8E=~QI&R=|e$q-?FHXC(qiW~9w+!7mKrPpb zB>f->dR&&g*dZq8rgG2Ijoea0cI-GBBHQYDzTcZLOrtkGf+v;N{Z0f@>zXetUwN`M z?|+F4XL71DU-nE!4Q^PMwC*%)(f-zYz9*kCRy$ZcuzrAc$=JUMMrMW5;-!S4%kw_F z($7Aij(;7IvU-}~xzX=cAWgr^2|dEeXnzgFFR)td_=Q}%%qe3`82|TwT}5}MkylpbE=rf4kyFcQEXE9OuBnbMR z;YG})q*pvi+5*}__nQWH749pGwNdQQ5f-TW8h8vec-%AzR%|OqezE&Yy~T_Xuxs~(baHTqU$tQ z-ccE511;b`D49w(ToCcHMsKU()m#$s8>UVAklGsq5!=STQW{~JeCG!bubGTi*6lj; za$^}69#3uoDIc%C@Z(q$wQ};xLmlirYw>&Z$pn_~s2S=8VhqX)bvw^a6@7KoaC4K1 zd}cR4;}VHEnW(jZ=?{F#lBRb2_5PvQG3ng#aHuoy ze)Q?4e7YrwO~}UiC?8$Li;h8=C`3otGq}I=|L&um3sY~_WSopuW>Jolb-xKRBdD}8 zE+b-~uF-4$=01093vdDJ6r4iyL;W@n-;(X$%kZ7hB?o(z12 z@R+0M`lz;kpn1W4zA5$T!pgnyqTclr@8)RB`~u7fU6SIwZeM`YI(lDk*t;Up!p*d? z37*-?dnLK?=v)gR62o1(@)>W~2~53{eC^6rmOifx0l)bBn-)Y~*dJ4c+wXF)C)jx(=hOIt{^*alIKAd?3XBRqMYHRhQ*-XK zAt@iT-kmTnq@yGmdyWMyEdubuq|57gLNF{Z;1-SVrAMS7j0?N;%mls@Kp~?M`s_*{ z5s}Y+oLQlaHfNUlMLy-z&m${Sk>WDdExqb^x#z0x5JyVRZS$wE1Dp8}Ot~=Hr*yL_ zdrM27iXRKO#0ubgFj9aiM~Fm1H1E1{nf93@)v%lk>RrzuD0EGgQ`vQ*}MS;9izD7H%Bqa;Rh zs$e1KjLx7Zf_al!aPxf+kvFq*pLVMfSbMkfSKm1eq0cijt$^WW99Fgt6rX`-y7ngc z?FGK)XHl_EkhwTsX+C4-ndSrI<<3)&8ch-BEoVDt-=ricK4x_u*kph)V|YIG$*YBv z;rA2+btVIKx{&<9bil=4XsA0fwxkh@XHM(|zcR&b>QQcGR>D3?N!S~07Ngh${gG!I z>i!ZNXoNOQGJP5Cn{BGQfko;NuM{JC|tRzTV0w5 zhgU`yW=vk#B{-(!6xNUmZ7*wqOMF*fqq50|3;9q5G8P;SNK(g43M-xAeZ4bXFLcZW zzW$$2##m`nJw}gjItnt$ciQ1MCA$;b{{x%lAw*HgjHoi7dFL=j=c1vcJ2@=Fzd_Sb z4sC$FN)pUr2B$TnbZHMYs{y!U?Xwi`J^kK!yKwIURQv-jsLpv@skq3L*XuW{{WE~m zZL!T2N|GrcvjOQF=FZ9~rs`-`cnF<4^0NO*QpteCI!36;Zf-c*<|bGR<;I$S^e)*L{NieOh)eMi%j5Sva~hwF|IkM@Yt-iK4@seno5WJS45MHcH9a9vg_pz zkpo{}mI<_&4?VBzzsQClGMlLJ?(>>nB0Wgml3kN|0%kLG?xJE~eRyOEH}33dc&wp^ z?REoh6@&+qdF+2#4Kz+DcY>5>$sDi4N^silBdqQ`fkc!huv&+-rmaywM_~C{y+lkQ zrT1CgfNd+-4wuX{{Z7)7u5rZF0O%xr^cpJY)*E3pCp+NtxEHKw>OJS{ROAWf%h-ZE zjFyd3IM1TMFio?CN2vTN&#nBfX2n5tT2jL$lvGeUFe%YLA)<^;4L^Js%1Sqfahybz zTS9?oclu@fW=dw!0hrchK~pqKy$66nvKpe~tsJ0)650Y~sSMh;^eW6gyYWD9#|wKrf9TH)jL1T&(*0vqEy0yX;J#9J94^C_F# zUN&|qdwCUuk8%(Ke$bX;cQ@Y}`^r68@I}gXP|1?xutCd%M$i4u?(T2^uT#_hX?O4q zy5X^JLbg7KjLnQ4k33D18Xc|DitU>u5O-ut=jL>^-MzP`04AeAsaj!zH|N4Bfu!|1 zrG52L6t!)FQc!fVH2?g0Hf;)F^sJ0LPfH+r;B4k%FXD``&e=O;eU!zt>wEN7-p@TF^zq~ttz8Qf*$?Fvb|CL?I@AKB zMKBylSa18g+rPhjWb5``ibMIUDz4Bmq4fNGVN`l>vSseG;Cavu@A?F(9vN!Hgyq(V z<_^(j0aG(_x8Z11K!JrC6J2xLw8i^mVdqA}Ca%yS!P0o5IDC-zP>&^7@aOD46p>I; zTMKc2JvneP3+@q)HR|3b8nBOYlzFsyEFW?<>!VmN6P;)8RCIWzEe-ZV9_^=ypR+x$ z2%BN1^?f@iU^(bBE7FLX?h%SmSDio@RH(tLO`;Uc=&m`~Z5a}#39<5m$588sp^#ak zYQC>&4p4H?rh-jXLd)h3W+vVzuYMjN_U!>ep;s=KpZ-hnui&ecPg$??Wb1#Q{LV+W zew5tHv+miqcSWAkxLT3nKnE9a6IDL_z2$}*s8R#*6yi=>WX!cBiE7+i&gvHkPdJrJ zT`6jI&kCDw(J1GoC7(vz9a7|lBB-VGSE&ap`g&vl^L!v>->u`S=tA`>Q5*kSMJA+u zlr5#{Vpm-dd+UlTd-42ybQD7`Z9`e))nD8jHb+1DBYS|&QBIo`(_J3a?SQkRH%&Sx z-Zzbi{6Krbgckzwb^aR)w4KNJxaU}Hjh4Hsis|_KoboIOEa{o~M&eS_h2O7>(iLZX zwVa2my4MM0OXv8| z@~nE$e!_LDn_2Iy0bWW0JdeqOf2it30d;y)-?$HE`s}2azX8J5SL#n@n^)aomdx7l zI+B!8k2_m2kdx3MfcKL2wyMA*(u(6T3;+uK6$5e}1gf^37 ztW051!~8HgKnX&7wy>Xhkvxu#7?{VV+CS=@4ss}lN3-NiVjc>K>YjoN(w*LyQntn{ zOnNXN!!e+rH8YO*$`aAV8NL_dOd^dCYb_%vHIiBC+3-QgW^75#V{!D}vKD}q4`i2g z;{~hnlnvodc&QB8*~sD3Me+7<@4oV^&7Tn%a<^0BC@GX43y-cR)eP-Xc`2#x!f}si ziN5wx@KwjG+{>SG{yyTU5ha@dDazV@S0{EI+@3!C%@?gkf;#k+2}?Nr?BHWeJC}ta<3#s`2=Op_A!MuCc;!aDD{_T)VKfj?9v| z`H1eU0DeCKCev`5xPO6kJA7Il=ix1zMac&~w--7=rh|BNaMfzkHFGeL3m`?bU9AkZ z`z}}${`Dzt|A`}}B{R*ClEJa|&D+yN>-mnF%}~p{+7opESuUM}2383y!RbB#y=t*IG|FIyb?% zY)KunGg499q?Az`4VQIs5gs%CgHzO|XefT3g+1l3;RscAXG+D?k%x)Q%6}-aqk)T- z4S38bt1{iIIjoxuMBh@bEzW$elf5uqqfx6jB2rYA@02sDjw+C{18+{rOpu)AJFzSq zE_jdCZ~5aYhc=nlTz>G1)$2SLpDz0`)M8=JYSny+;`ddg9gfg>+lbpN5jU!#*VEB$ zZFu9BZ+ZO`i|6JqpHb(ki~C&zJ`|++dtDo%86ud6de<+*+;9V>#bpQ_~Q${pft1@wB}7R2RHo z=gd`+D$`Ee%YhYh7}>3>VAC&I)u)8MS0&t5cz7$o)p&eCUmWbuGQmgt_slhhceS^kb6}cCnJ0PvZkNWC_FUI|Xwz-+tAu}~t zNMw&H-w)lk)Oq0@>#@QwGV!dDXRj)ORMt=X^4qRsqw?(5fZOTKnmK@P0Qs7*4(lP& z#YpJ%E>UDdGv#Pj>#_NQCI#JgSq4vMiFkV7;C+YH_t$}xGsA`0IPb#JLuuR#gId#* zWJ^hub_Q~wzC3V#o-{{5foYPD&>5Du@^S0?{QE;1nF&{MvUvY8FOYh6YxL-AM+v-k zUG{5iut)*e-GR-1L};^ez7(83(lYOlyzl8oF~$#**`#MaYV?zUy39LTvHvl-F{D_PG}ABnJPFmNy*Dfq8*nm} zsN@_PgO4)!%pvNT;reLNU96ptW1uC>Y}-7g)p-M(@7*vr(31=)8a>aPJ6?A`FIFj6 z6;Fp8`E|0wYRJ#>hc}Fs5fvX&MZU`8U6FeuGr=Yil7{_=oIe zuZU+!Ue_9IfA!zL|8GFRtnnHZ@_e}(%ICLMnepZr;Ej=6uWMI5dJ>@aBWC44H~jmJ z)_o14wE7Tgpf+9V$0H}(?My6M{yCQ7C)`MexJe0SM5G$mSM)z`!(o=$ zIVDW}^oH&2ZTgW!)A;j~R^VD}3mzwwo0BaqZ_mNwf>Osqzy9l%i%h#E|3iIv`bhwQ z^;`*&K;f6uiHFMaCRXfX+xxkx3fzu7$KvGv7s3?OW=p*oIb2N|Fw9?DY)>Mw4ixd6 zGZu#oRl2JR{`v>FqL%g{AgXng{jtDgtw_9<^f#N>+N3@Ab92v@r9G5p`K;5Fr{iu=8@BP!9 z`80dcwHImjVOuAA-0x-mAnV!gVF z6=0&C$?%1o{9RbvHRLL;fdjiMO{*fMzC; zv3Nal*j6KQ_`mn~3!-DkN7EE|9^Cx*jQJb46NCJoH;DOg)IThuK*+^_9K<<=hTyI6 zNjCT&|IU57c#kN}3UhzNK#2?b4_Hg>TiXi~39lSr_8S_je|)g_|FQSw?@;gk|0hHp zp^lR!%T$UMOE_T$GfGh>OA?YjB+Hn>kYz|Jbuf~$WKUT$V;#mWW$a6q88a9fjArbN zVaD)%yFd52@6UZ-*Y^+jUf1^*muoK9crUN*`PiPPAHIp~)sR^=@{rE37yhrSu>g27 zv)?`5KL5`<^Wg_Bp3*N|_TQA<6@XL(Tcle^{3qX5DgxY22Zjs!+>6ZQ);1v2)GC-UfuxURz;24ToV>`3t$qD+cb|ng&!QIsRWZI)$ z*-Ecp8O>{4&tyaOezjy|ZoGFDhs%C~O&We#`Z#zRfbj#ie4htO@L?M{j9YfTK-K%L zfjyzyvDb7rh5(8Pd&C|OQY)!YlLUp#<$Pmen;IW)%_QgVpTKPOJPW13t5x>)oNtSDr`S$FD{_(+lMF7GFpihFIyHJ{@epIe|$=IeBXAO zwpJcHQR1fnfIk(0M4v9o_#;I>~Zo>2wQZm z)3fg=1&47#hb=X*^VF+T>pUauu4h*Wp_ZnLiqqHni^Vk)RmDX-DT)FM&|+Cf0Z6>w zYJ1K>z2C3vN%dKte0XARK{&i~YlN)svTV3~JS45fasKF+QQ^=t^77 zx#LKl{$Fuih^Uu@$VAEQ=qR?>zIv|N2toE#Fspf@MP@Al1ih%osDzR#P zAQkD@u~s5M{l!8o!wKl34r9grk$BKAtT@`N@uCbopC$u$8y=o9vJ-wiQ!RoN-bdYD z>(Rj`t}ub70;v-}oaq43rH+zu)2Gnv0;Wra?eL3f>UB*c)jn^#-W+%Bb;5LrN|U1< zOgH!OKeY|uz!hZA7z(i_7b)*MUqUux#(g@n<-is%M41DsP}fA0w;tU?Zm_uRTvggj zTp>ZIjv0>N0qaIpArSP{ z(h|tM5pZm7dsS_B^~X0!r}XU)R4LtQMeb!^f2|YwrbFKp!`!+K;an51=WUw#)wic> zjGl<{b?djGOKpErv)PJi>^+KFr_0)`_ip`}<)0DW>fv)$Y<@Ws+GhNl>=|RC{bd5s zfFVdn@U>w+jei_9kv>@USzZ>%)UuF6Yd?NnDMXROD#$#DsU5KdoT#$<{lkpKJX7I` z%`LFGhpLg%;e%Sj)5RhIm6COH5m2PO>*o=5Yj_KE`ltVV>J3~Zb@P&)Lf{A;@>z=R z?-yIu>E=c_hpSv$tXneUU81zNz736rex_CRy0oMDup%C^j4y^qzwJpSsSjF$?j;kl1mQL^Pu^{wco>PjCQjyQhia(qh{4CHi1iT2h)MuS!1T&iugrs>pBl z8K-~(_Pe#Bm}FosVl3s#Su9**^qyGwT6P{>QEB2==QRqVN#8WPGP>bZSoBf9x32M_ zZ)d)?ib#V-l8Car%GQ%HIQLt;=KIK&Xa4feBzx|D*gP!2iW@fBA`df;bvEakd?xwt zVS3<&*ooK9;<)uk1~-3^yahVHk*JYAs46>&9v}J^7{FDL3tT(?2G(?=CEntTb=#@h zx~B2_3x)O46lI+i8QsSZyao&O2`4v>3mN)g*>SF4HBbsc>OJbN;`*EyB6A7@9;EBv zjW``E`_H=-@}Ccwgt25j$k*&Tt&=#HWj4|Bzpb8vXP61@O3gip0<%2RaAy=+q+pfT zeeJPY(gDZwp^V%_f1i^b0ze}GoaSOj1GP}W)$LmQ&Bc_D-C8zVcKGkrmCr#WgR~2$ zI*<-dE5f$xE;1cu&&|lk)s>FTneATyErI9W*vV+&WTTN4F&UYx4sBMyW>k;GUuQcpv+#Ic4whg zbbMji#obGw+GY5t-V4a=ef&c+hu?V7rke&uwD5ChcH0GD*N2G^v4=g6JYiiQp~jxc zJCTuV-qo$)He$RdCJTd&m~0h}wC)U?+Z#c;#)w3_zEDR;Je=9NlDsxH^B%UFti`EF zRe##^kz@ZE;@QgO19Jta*EV1IazueFbeV3gJ46RA(9t(6nz@nP`k@r7b(ZIzY_v)&4+RaJX{7!GHG8k#Z?sq@6{LmncXxnNr zu4e^zgP>SyMidx5LQ`CBFTnylY*sL;~hccJpLQg53smT@L@;v)|_~4X$EH_p|S60uDy&; z-G&U$l5m_bM^Iu8NX!h=kEqS`l*u>@J7PL+I5DN9)pOuTvad>_oX^`xG<4PemZd4h9&gP0S zWQ(ALcOg~zeHM;RNrS!%N^UD9$w)bLG+FUbA#tG?$;x3?gNBrft^6kJs!SQT`k*tJ z4A;-`$*Q>#4Oky@UE}(>_=!x8S3~DFSH|kfSUU9Q%;q(GI71uHs(E43pk4W?^DNb$ zTigXPA6~Vv8(U&!8aFLq?+w65iEE&+-HaPTExiDO@i%9rQ| zI9SJ+A_)y?+;5NLJoj5C?w9e?V8Exk+B16}d=+wNfbO%(dKQ|N>mT;XH)1)K^9zsX z^2&^{VNh5-yfDK_??H`$ljPPMg7kx8os~kiOP9aolVNkTiK*7A2~auxEiuAH6|5|BMJUT^ z4IMIhhx^r!u)kKh6|p|32&OwK{Z5wCqMYJcSA@I4LYKdFM`6E$k4Zqdev>X{uv=Sb)wVqv2p&*IXxv|0v6KY?L=Ia zjR+}eC^VoSo0v_XVOH!(U+wER2Y$54V4ds%5uUOscnybg?<^)+{5YzHjYiGH#A0($ z3yULfEl{0rWgO^M%9QVq<$9QR9p%;-)QvDxXwi*S${(dJ8L+-P)e;5bGH0-MM>$M? zcObK@i(`GbYi4@m;`HN-`s~__5TEKv)`oq=@!SU4_}}JzT!$h?q~ZFsyB=<#@5KBX zpgU#k-N_TpGou2MIE#L|5C4S{@|P#FJKdigo4SypcqlQ5GfxVNX)iDH2`yfXCc}nu z`o`tIOM45^^5PFt9_gaBta+P4OBfql_#%D*JW*`sF0DkW{_A@6Acxb2rzF%+T_St* ze@1cY+3zf~aF)HB_jF}LOzjCEMw%DsW}E%j5h+rwh`U_GOlM#UfR;ja8Y^9=TMHbx zpERU~tq?ujl;|Tnb^G6PUxKA}Sq&p}622rk)N$d1j~ljpT)vA%2v06?{p3`g>6gj2 zpJn0zua)kSzf!q>-y4ijU8>|tjQXFj<^&r4BUYo(kMvY@TdoJyd6Mv0|BjFO)z040 z0fKO!&!SRWEbaM9&xRpgnb7qB<6JBYDNVc`JW3dR-U7=70%>|!D|2|A@&x=#d@ih* zu_-@XtTp7yE%tvxe89WK?GG$!45|O?>br3|4WK7p#& ztIgR+gsxH6-Jr#kkTAyGhais$+S6wt{n?Zb#&(ZaY(ijbKh$w3*KS$Us(hBzB`m@7XB@738 zN+SbB=6b%9pTECNSsHJ;>!irJjcuWX3Z&Kd6&)hH4N5zk@33GX7+$3cKS|TKA_%*a zu=OT&qL@K8vbU)IH9aiDBLhAxn^xrDXs9%=r?jNiDWQcv9fsM6V_0SI?qp=}*Bb?X zxv5odAVlD49^-G|rALH$S|S3rH{Tuh&W&3VL#sNOdHePVRsH9b{r9+ zBte!MX3b_+M0OOL{XqV>nI$fZcSmVd$U4}srHA=y%p4OthS3Da$W zy-hqw&)vyQ7vmU-!K8+94`lS;#j}{LCa5aZbb-e%cpxbCNUH@XcKQLoOr2RLQ;1vC zV4qfDt1Fkqt7j|_1nH!^*c@Sfy+2eUHby;^aHL2*sEqvy1+M$dMvNB6`C}vGU6}*x z`|i6mt#jsP6jY9@oqaAkWWe2_E5-FSlSVBG^Sk9y1(I8rW(_kNCb+kfs$@roilJom z!Z}M`sTiQwDDWG1!o)ou5ZtL-9uk7TerIwn%Azh-l6(U2=Gvq+So@PdFWkbeUb5y@ zIqDlVRxhzI@!>6sQWwx1gh%P8c*B6vEb{%P;Xp>s9jjiXkEJNcc!4a0)}A4DtW<0q zM9%71p*HWpX=IJIk$2atkB(F_?Z2>&6suNui;_Q#Pc*#+XR>mz=Y6R3CmRa!4vf6| zwGZ+2h|k8*@?1xHO*|6I);ddUV;DA=zaE$%6B;t%v3}ext+%#H{g)&ps^Cm)>*Y~Y zpZJ{Yp^qpT0n}y24p#HT1+YfT+Htnv>}-ClIq@f$Rr4sgz%Kq(GK6)>d@?FMQH%9h z%Hn>5yVh-o%^UjU7E!{eZb3}FCeB1tQ4BfTIGSdO32r29nNr^ zS6WUaPB`)Eai2tMIun?cR!Wwbocz^GqS9`|uS7~`rm~@gNU?Llr7v?b1MSQmrjuGO zVXC0+VfJq^KPU@TD6&f|GL2c28fst_%2c;^ej&>ELe}5{O^$}N=LekJ`=85l)>}0w z!);#LD0PM|V^Wj&I`RaoK~l#W3we-*!?V|)bNmb^R_|tU`N7+_gtTt883TQ^+{1ii zz?eZnpZ=@1!!}*D3!8tUwug+D{F>WaKmoyC;Pbj zI~b&(ScQS9tvEeG^*>YW2lTmflvb0#F7Rom-=zK)4t+EL$c z&)iLB+bfc0>o23_8g`ZK6g_rt>&hmVzb%n4i*N+nv$OIWKvX!V1`{KCeg3=N-h3?vk)6qE6AVYRf7{Y1+%)V-`| z0Tb%9Hz3B`xFcKt91u67(_+(p3x(E>W$rxD*VVMapvPnH68fj`w($scRGa}Eutc)dhOo4cO~LJTZZ2_PWPsO?D&fVdgcG4AY= zZ&u%9w-SV%KBxno88MX03*RNdhB0JVLqoa~9z(sIod`23z1k!|i+&z98@}8gzi%rt z2c|I;5K%CP$HT~m)O0g0mgrcXS%*=XPy_SkPPGBQ3nu%TnTOK1P`(&um(u1`+c(F|cFO(5Ebi zz*L~;N}xj~B|3MfCE`|fev*H;*sX*qq0gMS#&EF*uI?Al4Qlz5OMBdvSy!c*x4CC? zd%WzbbC?67{tK7XvK*bjew@$ca-+}EA>fg4wD;Z9n%_-r*)|!)>t~hX8PUXcmr0 z&KUr9;-cC9vN*zhC9mM&&J9;I7mqw(ISnjBw1@?EKBXmnA#aj_4Ay!%4M+zgI#UM$uC&O!POpLV?w&ftK^qc1wC&a6iW+XMz0N&6nqf} zF602KsTyxSj`nE%%7Ntz#gV!v; zenOvb=d9a~)6A4}#po%HTUjZ)U-2@A{4+{?lCruoU|NEZ^f2Dw-SyKsl0o;QBg9#<9e1XS928 zd)K4%UEpTClnsyIW5et#fJT@yGeZ|`m+AQ+0Gm!|w|>{aVHC&g3EDa4-I?K>pWlG) zv+68u-p*>5TGqi<8s87KANl|wEWEu8JE(_IWv+Pm?e$9D!`HnnRz(h%syP86xN$q~ zwMtoGek%ZwrusnJlY3Baz29&!q7C1H|4VDn1+dNLqrmj;tBifU>xNsg*Lk#eL$W`^ z&jwsxj___~l;w;Na6^4$1}8$F#VgFlzm=@eve|H!Z0=VOx-XHM@29MfZEr@5OvS8@M=ll(=Gu$IimCf0~u{OycKy?kr{yl?yt2(PN6`7QW`dWU4-b&9$3AJKRTmqwVH|H?)pR96`Abjfiu9&7 z!i9c!MwWoGRv{#Q%}B^7*3f5421@xV!7u0IacDGx^ECE4R8q}xd#K@=FQzrE7JZuh z{>D3gjiF-KKhfkiZA#Z-&qS{VkJ0AH`L>gT(y3qne!~*B5|~sT0z--_Sfk(JLw}Q7 zcNYoxtm)PF=!x90L|9(LN+f4)VeQt;z2+|KGfQ$51+1quY-S|E0yoQn5R6BAk5P*= zHWW}#VCnwj(45BkGsVcC9&JkXNoA!Xiykv1#!u30NJG%9PXM%8asHwFfN|KOmY;}q z`+K+EhT8e?f)HbxAj(Rz&zS6VbndpiM!(kP?cL%XuwAHJ086W%-!;HL4(i^eLig{0 zD1qBbU3}$N@lQ?B;+y?J$T80)4TsM5_U+rB14(St>>z z-PMsW?h`HYQrjq{@#)sW&%Y=A>^c|_SR1OQOL*4YIJMWye0k~%;%JHqqneU#xkwRb zJThCIrKdp5^hR8mqS`5bV~>@8Wg;Z|pPMO)DGB%*HHpmL#|T@nZE|B?MQOcrzjY((kP6=A)kCiK) z^Xrmh2diM4T#m7X13UJGmJWp8k}1mqzj$QuLspt!F6>9T0DsEsGltfS4q_T(@$1QK zk&RyX)j#@M?*9gcFdGcF&V{$%hQ({JheF|BLxI0 z(fLwSVm$eYx_xPCu&m6i&u6ygM!y7W*jzef(i;BNjBrM4)T!6%-MGeVtP3FuHMVsx z=1}SWalg}S+vjo@f0|b%GE1jY=_wf8NH~VQkHuMgra>vFO)oB$;coH?=jB%VQv7+^ zj9sg>*L_EAM^#Yca*F?vxc z8jT#Zkk<80J&%u0l!!6i4~hAQJK8oJXig7u;&Apl4}Hl+_;%5%)0zv0CBo^wdH2~g>q)t;r- z`V< zBO7;ESo2zAmdaj2vtk{9=Pt?NJfvCgqQJ66fbPEd&_?rwf6CO2CAI14S3Vw})!u=^ z2l}j;)=!5MYuj7=hIOt8l{Y|VhOS22&od#t%Ef-| zY&<&Lz$mQ@dDNfqlC6ex|FM~SsG$ngSr;WjO^I!=J-*Fl8e-Vb8?CJUIi-DN zcfocqRALRc7t(Dqyp27|MQK*G?4KO5#@*geQO@-5To|{{k_Vh*q~<-qr6~reYr&ct zV81m0jG4G;HqSCL#w2rZ764tf$EHnwVaTWm`Vhy`Y8iffmM`q_oV+xzvwvp+9KUPr z$ozoi#G*)}yw6A@f{8sV=UJ?BQT=B?Wo!d7L4Copz$4V6C_gF7*=L>8LCbKeOGJ@S z7V{LP?_F+q#chY9Q5NYIE?U<#z$Vp&A?<9xjQbCSPFr?o!I7_=onU|-A zRKDQ5^%!&~Kc0^FTQsKF+*i%AEA4(%5CVZoI&f~g)6v8k%^)$Ih)}V(wwJW(_}1RU zoo`FN#q3m`i%jNuF)>o8uP-$uYr}uWh}UL+o8ZudAi7NOD)rYxnmBq)whk2Y9%#0y zFnvzE*BzRlRg3z}q94@I@b?+buPlc7gSFxUifl6cUAuRzFQar@z zjG>tWQ3sWNJ{nP4IHqgvrrYGsaXw81_xc~I^j2-U+vXp_E&(vT-i!4;;4Yg7l4{{K zHG4q^e><*i-}@=T*Y2jE@b!GN!-^C)lKewJ>tJd}ek|90-UhQ)do?7e9(FRon?9^ zN(9|&6`DLiNIJ0b*tPh2T~SG6CkDzY5%g)8JPN@wyt%$nHxc6@57I8?s~Jb&$Q)nS zxkE!OTCue=_uO=W$-pAP!k4pe4Zme`c=j#}iIfMj$BV~`!m}zo%&>vVU0ew%cWIi> zXpvpUdIj3BP1-ffJFGrlDt`i$8-g43XSB91Br;qNfpIgZZIH2T7-BbZAl|2mP_mSi z84~eO1e6*I5yIIvKOBeNLa+RV35oFJXG;?s;*~vi?x6>@rhS}J@nNGn&i3SF=5njA zPJ*RmnstvpM}M`NTptTV&5~r}j?5QoFs^!JfOJ6P?C~}WnYm}DX~`aD=IgKQo(|Es zKg}0VIY4<@CAx-eGo{|oGo98mpSwZ0+%T(7i`pWG&vW$H!9MzaxN3_gv=i47-|K@h ze}}~*t4bV;3I05Uoo}wg$SzBooOK>&MaZa~>aw>HXJ52ybB=FX|tn=IykDaJq1|M+!JSO;*Q% zo`$yRz2f!=Y0_7Jx<+~HOQ#QNacUxDbthU^-xNcPT#4SZo8P4D^FX5m`etC1fuDNY zimZ^tDGj%Inq7la0VNT2T|Y{ne#9N$H`%p4B3@r(bWA0suAbi2wvS!p>YuS=p~P0& zSx(d0UTWHJfwqULo@7>q=mB%P?KA9{Lg)G;5DDtjsa5-heQq?u0 zKB&7|(#k|Q8rL=CIN70vxYPUu7Qt^Jw)IPXcc|@i07IL+5e;Xth{yXP- zXnTuC<>_2R>Kw}_m+mji(GubhS4wvB32*^He+AqRenTqGK`*m}w7phO}0Ta@pn$vt|DA?8=$huFVTh9Uf>T^{M7D zg}EoMXha-$yLsz@eX%7-&n6zs)@Qw|7=zobCgwMu)n-&fadgfNxoHiX28Hzmpg?DrJaGpCys0oxzTj$r|=i zwq7F=4_$P=_#Wh?&r`zAY#Q0h`DZAtju3^G(lF+jRd&7qBB#i-3yn=$(HQ3j!2idI zFsA3BUs~gt@|EfK0n25*+hglf0u4!>Jv! z86D1VMD|csr$CLR@O=@5t>8lPb?LxcBMYBvU+18vr}8kp23L3&x7q{Uj9Cev(igN( z;eMYIgA_0Mo_!d95n5XHvLwK-qR+eG`p*_v2LHn9PubGvDi&38ZORKa4J6|aKDOT# zr9KQkYRB!bcCdBw^>r$6r8XKS`AOj&o@=aIWce+gB_BII-=;X-pR3wwc8^JhaLS7P zUhq>e*ETeFzBdWQyzfv8?uuMc^1&A$S^p!k=h7$A5EXxtnvilVp(i@D=FZu)@Mt? z!P>Hfue>=Dh;>mTH=McVNA-FcKHp}&?z;OD%t}eDZ*oSXkn&e;A_Ublx5lr}JS*|< z*s6skCRAsYS=f12#K#{ooSy~%;6PUq7*N zo1%kR+boD!&etq}>O{x2jX^lGI`Ql3;ajFE{*td!5hg%+b73z2I+xLOxV=C4Y?&uf0C3y^vea%!v&w{X{m~I@42@D>7f8 z<+W?So87c(fGIy#<cVA;3 z1t~8~8pXb~2tr4?#;)8;V+;tBN>aEd}#spg{XpU;>yoza(mjc{gUP%AgbjcWKwC7wp|Nl^o?{ zm`+g0ir|L}0)Z)jrah3NGOg>z*8_Cx|IDtC;~hSKd2&qr_`}JTp0qau5-;HXhjq(>?fA>!w4b5?MQz-lPN-g@$M$8i@fxIQ{!KH%%TCL!c{}R#ryao_MkuPSJ|D_I3 z0|HJwfINaf|4UCV2w>Ht$LIez`k$C|sWbq|{@=fTqAhQeskV*S*jAgrexg(~a!_7# zyU=UrnCY(Ato3MH)?c#`itOE0+taYxUZ=|;jKX}(-M(u%eQw?5^TuU-&A_dMzspy4tS5)M`rf8|&ImUEV%8lrcY{^N-sKy#)L($AGnazxr*8 zO>iR20>8(M(3EenKMLO;4_hBF&ad`TI_lG_6>wS7ETlW20d+g;hj-P` zushQ|s2FC%f$QU(kr!H3)sOd8UjfJL71DE@up@H2FZr#JRST9rP$#Lqq-8basXrg;0N<%S@;haj?c zSaQF+bw?au27`VL=l3S$OPl=$ zI+sr9Xq7bw1A#sI84|bS0aD=i3%1u#67*?-TgtIsA(N3pFQ|eQuU9EQ8ubg*{PuEj zI#d}cn=hot6)6jLNj|NJj|r{T649itl`*^D$qE;4qanjoNvE(!zuuexl=_}W3RLzI zg!s%>pX)Y()pM%J5o+&&--uINl`63x=`o6I}e*w-gY1__Nf^&SD96TP4U({?-RWo@%j zmhibI60|8GMB#Jp1vg56JWXD8G#|~3pLaRZcRAtei9$hO%*MQ|>blODqg_ER;rD5aHht7K0x$)97Qv3oVRZiZp^Tue@TyeMsl)ePS z=x8luBeo~?EuGWKRXH#K0jlu(>yM{}>(l)f zkLF9`1NUxZ=zF-dXYmJek0E(U$42qb@t2+m&*8r*0C7d|Y0K&k|ET>^*-_euc&0B+ zs`%?R1;@Xgt6fgGdO_GS?5YsN``CVn;Qson1+ONBDTkgI@$wxy??j;UiN==_iCzbDC;|r<5)*0ZxgC*AtfZGk8`y^gE z@C~t>6ybYmIEt_OTj@==Bc0k@<;jB~XqmZw9QCeumax9Pyvp2}rI;`H{Q*t|$gKV< zr0@-b6o`-8okxG*!=#_H@1M*)3S_4!-_0%FtK4tL#`XR#Z1=avUr ziFmCFrkWhPy8!Q930hgOLxZ6WM>%n|u^0W?<{|t3TtcjW$<#hwYAlI*30=kC=9hBo zuRGWO$OQ9iz$~YkgWm0}Aw#~O@NSE-8}gFl({CFGULuth^R!5f@Xvkpp*&!e1dBBU z@M+a<>YHWLP8RLl7fLjWDZ^i#YTy6#6yJWIlZQ7aqr@`pf=lym@$#*V4D6jYtEP7ainq|$v5Xqga|LB6jK1+Ga( z4pqWotc>vnMuXa{2nC>qZ!=E@j=T`c{iifhfJzHPx$hFG$I)Xdmjh`;U7!mzp}L0|@b zu+2*(+=y3{;m>=CUB0SR`%`!E>a7MR-~*J29QUsk<7UGwXQg!MI##1G^zR^CTpVzQ zgfkN?E2FQ*@B1}IU*gK!vR^(^CtdksPGIWh?nk!%h*9}d+3!wG#RDcXAy?I}y_?r| zMSGw{MwF~xuzC!?Q2yXbQg+mS?4_sW%4b$Yf+gIPeaoU;;d4;iAXOWsh41n)`oTjG z!BhIB2KODfWpUMa1-z@&YDF%5QyMJTeUR9EP=7t=mA3Ncaj~w@3CetUJo!pA>j4N> zTY|HkS$=*snB}>y6$gpE7%-y!2jf(};{u}7%dX4k@`ymb*I>K6Tb-=CfJ|xhKG(LD z&Wqy2dgIx)by?Q{Wu|dJXx2#}!|IbDT@kM=Lt*@wSaURTl%F&G+wlum1WrC_p&nvB zK!`!(BTUTr+B8JiT;e&ht3S9LhWocav{@B609M2_gouWnyfA6!jk~{Ms%w|rYxgC5 z;>+^FX3@e;dQUY;=HJI_dIs=x*CwLqL*WzC%dU^YyP6T_7!t*6KYp`&Od1)i4$P3` zx_bQkoIbAs4B5J|x)qhbUDW@Gc-(jke2@OZW{upxzZ+Pr>P&$3SC#D>_MZ+=eZU#% zVXh1H{SPZ~vnpUss_1Hvq5kV&zW0l>`Ce5ph>oZHM-@@p1qfB<{g&FQ|HA=GvIN$W zjY=9V@ZZ-AR3-oavi~1=+IDHpKvh+-IYx(8n#6JTkb7&Qp_pO zxqNa2p$uNqrttSk?8HONo-&I#9;8PD0j+(`_HO|Zn)QzX(ixpU9qU#0IGh%zowuKT zPW-?Xd)f0{d|B1v=6Wk%_dLq){{lxJ)Vo8%ruF(~NA*>Y2U33QIf9>NENVvXZ+R0b zGL{*!vvs{uP`bfUA2ba9b9W%KWn@o@Q<%4OaHuNgi-oQLZ13QD7f>dq|2ykoe6KVQ z;`qVM?@X58ac|jNKtzNBhNG&ZzBgY#SEDYwvWx!s^~#;ewT&0wR)#sy?@gU~rVdUl zBO%?_CyxBFer3|Qv_h2b7&N+7%kwxMxaV)?*qxo9<#eev=~0IMC1M)e85Q*Zm($ys zd0^f=+rRWe?dE`S3{Yc)KLR=+moQ(NN`FndC{d>Dyac?$pE4m5iTXzl9KQLwVw?%& zg*pX*CDP_xFLkIlQ722T?Y2=Dj0LRinRfB@y-$-6E1ro%l&`*+oahA)(l?n`_&+3H z)>!pa^O1Tb&QB}@5d)iTGS9$NBTiY?vv_|`Io~-vFfx?#e570H$jx*7=kKtO=a8pI z%J+tbWs7Rd1-&qu2dG76@zi#+7U!AkG*c=kojEe4BJAp(v6HLM_kIAU&F~+`)xrfm zNndrQ;D_zjO*++Fi+ui_yS;Y9If5@E28d#amXy@qnH33@n=5@>zZV_57kfFc%@O2N z=GdNU1UB@+CGaNiPlsmlGi7%IgrzR+d-cl~=ji9E!uA25Gz93!euTbv4q!ccmK0;L-1(94~=+1&0<-0D=^mY=b zKWF9&KuCbTH2ZRCg))*dFBGsDs!V{7ixPBN?@yOXJ6`|ST3u)8+yvY!muHl(Hs|ni z1F>$~Ts~JUY$BPcT#lcm{32O4_M5Vl?N{-k(&jvUiFxH`%*(Zi^S87w89Ag>Tu{7O z1pi#eIv5&q{6n0eiQu7(n3#DE_h=`_E~|+8ja76!8h6KsSbcZy1=zhY_=(IuVXb36 zBqx>%p#VFd)+@dn`a$i0n}}IW?+DbV$bBOL|aadsx+;ks#PNmRE5N6hxlR8yn| zUa#Mnv4;;!xo>E046i)6Y0JC%uXD)d$uMAh@^9oWT^p%5?p9{zZ0Hy1&@>SAsPjYo zRd-Y6--9iiqo2Gev8tCYI+zi5C$?4!BkbS?Z@pSBY}Gd$ow*aWm8<;r;FZ!`)mYou z!~+GJxU6Zu6pg3+_)VjaBU$IdR^uZ@Bl(~$K4!QP89DLNp(n4uRU;;f^x$6kvc{+x z%Ijk~*81|rh8r5yA~W*}-0vZ*xU}-w&O6box$B_SxomiKy~X|GDokG)4dAegmnZeb zS+1Y@SI$DZ)7~11pvsN_{d#JL)?%P_rzqL0NZ z=Gm8%z3q1-WwznMrvX?1aqK|Aop!dtCqalGb0^yy10{x>JflI%7zCeV$H{Oe4=Jom zJxRZkZ3%xa4mmh7W%kerS`Pnx?z67EciO)yxx+e50vqT1M`_f#WyPrK3#ylyuif{J zMxh6O;75M$y9V%eb_It%C~JGT*h?1i(gLR$C)3w}F%x>AsF{&$q$_v$DsUj6uSsV_nj-Tv3X)ney}{I&Qexs3w@ zixy5tYD(5*koh&~XQ^7Fty{&*`zNm7Y#mX? z370ijc7MCii+kaI@j{Nn#NE=zi?#M&Yk!%k<#>wgwyU&~EV94zR_X;$Cgq5w9Wd&@ zq2b)`%PRn4D=EkMly;pBhhbS~D8_;+<6?{{N!Npz*o)x1La9h@AM}K?;_IlRublsaB^iA0^_3yY4*KQAHs-{0 z9zT-|8IJb3_4oO&c}uBuJhJz=v-C-jZj@CPcSv{HGhV+#Z2b5aWb@h*k*ldPEL6INhqY8PNY#)jn1^7#j<61q%3O;og;r%rh z5>~4U*G`TOsZ-?@^MD`$quRPgJO@VwSo+SNHXy^w*61Pbjf@8LOfg3+qy0x3JiC|+ z^GKV?4V>Oj^#v^`{(ISp;G-+_r_%H!e-W8xgZrf?Nkt!y&H21@QCIw5WyIvzZ=^;8 z`hk|ND{@=!MR%5)&n&EsR2_+jpSzJ=VcKxS?%#!hv-SZWr|d!evG&8wpTz}`sph@x z|I^l&2PBzwahImH>NusQDcWmgrD_AwG=a9~UQp3#p0+zdcY-_)ow>1f$~JuIo?vWrx-`MT|tw zts36l{x5T~v4pX~4baeu5aKYcn3Tk~i+TZi3X@IjssVvxS;Iw25L>qJD+u*A2 zz?B07Zk?�#UlUF}#1_Sh~$u|6`7S2=DHXPD~kn zpyVG&q`*PW#xNSl%BKAo$#C8M9U`A^ztY+g8ePMa0?IzM${aR1o)aPe#)6MYROeh^ z7gk5EZj>zHHx5+ok07Ny`!>cS{n+W6QNK^)+3BtH%$kOvkv~*Z_kD1Rs%j2gJtujh zsj&s3iEusear32rCh*rHK@%q<+GHWJ`=5^;erkAXY*zLpZ?q6bFGj?k%aG3a>)RMm zK`MDi$g1a-VAowvR<(~g*)Eh|1SQb#5@{}yyBhW4?zwMoLsZ&bD~hn)^~=_ueP*xj z`1R25;rY#4gFs{RLUhSud1RcoP zZ6SC5_{YNs^at?a^tHJNdDmUnux~sAE=fPv&yB_|gGb_W_*yieiqBFm^3A#bM*4q` z!b0m!*D1M-E&F8+(soqD$Dca$d%afU-J_m&xW&)-WW_yu!QWoO(9i)s$dUz1_I?B) z(n|}z5Jo{Qfu`Th`MOCveTSo5M4Jp`4Gsw~xnp>1jNgpuqaMfPaFAVRg|Tn9K{D5hX*&=kr!-TDhA-n= z!lAdNxMditWM5`CCotWLdbNDC;4%7P>BKH&=aTy^6rkvvM z{dno0`fp$70prL9AgfmNQx693e_0wSYFRJX*Lk@d>xz2L-2#!ZlK)nG`o??-6sVor zWz))u!0+jbpl*RkA3pM}4mA#LNKC7c{jhc@N~`gGF)A_%Tz)!CN@KImw}zlk2WaWd z+LwAdAn4g5a9`s<+i16v)W*$}Va@d!O%>v-4iR{2sT+xI$X`_a-}cIrHaUp(u3J{K zfBe3j{G#P${J>V0KyFqAz8U~j+2;E9KZP3{%lhJSeh|LKg)5U`%cFbltg`(j?3}J8 zN218_32}{2L{z475V+ejK;;~R*wq|WrG|ez_x%A>2bGupp-<6&00gqutg^7k!C+^Q&?|{oTrpoZO8L|p( zIhR(kI61I3*)X-fHsv1iSZ2X}UF2@?;hb;sPqJa3;j8s<8%`(nUaRJ6t)|`@(De4_ z6?590dHd1<3~=Jc*UpW;bZqNn;Rgqm+kL?1z)6$aS;LS=p=Ui@;W+x-5?{ni760~hT_PPv6=w%dyoDsSt! za3DD;|CmQ#?TDfsAr3MpWl!n+e0xAzL;2$>j|)WT$x@pc&R^DtG}nfPq~eT}YxFvR z@L^3jwdQyA^6o89w;pnOS5)Iw+oI654dN7{jeL`#uS_2uUiDw&`M)aJN+LbWelq#) zoXflOE-CvawB|POvmcD`WBis}GxE9CzkH`&_W5gbrNos|dsS4KRfc8wA6h$!AEV%l z3Ol4~0Y7N?R|WnD)u%sTVtnave65%7PbwALPOZksSFEi~t%r|WPW+`gN~O+_tUE&j>U)ncD3x%=uh1l4~(cRZ$(Crf^x53UJe}$Y=~>CG;O4N zJ+w?dzl5GtH_50`zI$SKXa>cgENq`hGDRfLWD%FMqQaf&uI5|yNNOQD()t@k$FlD3 z?ACD_iurMjxxUKO@hjHw&4PciId%HuFj(}iP}Xqz;OOyNmW-Nr<5~E=8|Dih`C>Y8 zmA-qfA7qq@%a8w`-~2ry{OgGE= zpTQux$x<_IG725RxqFSO0tYYr@Z?6nL)j@x1nEgw|BZw<5!ZGsW%??58rq$*+T`({ z&i^F5j@ZZP4K4NEFC0$0mhEp0l#6%O$i?q>%C6mx%|`}Av>ixDvWnPKv6b<3nXy7+*Rvisr1|GU6n{JDV2%&+Z`Xg=BE$kQ+NI3D7nC@ zSdD7}Onav3FQXpG8YRbYo+WwJ|E@)NL9x>*V~$EhEbmQI{P1*n--|1oZSzZ~B1pug zT>W3SX`KbKLs-FZ5?0S|DZ;{Ep1mui<4P$Ecs6uR@UA9nVgrWV=4%towTbE5jlIeD zFMd1@c3%5bvZkE*Fb{Qh(}z%q^zledOhubKZm||_Gt+hKURCO>srBLmI18R&k65WQ zbtzW(-O`^}R2(NQ_fw10{K7Q(E#L{F#;o-JtGq$IZN7TIqLWnH^oQo^p{N0sA8MTB zqp?s(R`fmfrcK$LO1x;6wguwrzS1^O<*~7KhW*We*>boWLi=f#d zIhd7jSf={Ogpf|&4uh)W8tsP`6bJX#1iT2MADR^OhT$?VbkrW|WC0OdoxhZI_hiMP zNQYI=A3QZ%?)GME!@VYc2$HP(PuV2sf&H{s&*s0kKXB$z+)c3W{k3)S+xn8aLr3~e zk9>-4Ul|*@c9A7vfKMvEkbQbK<8_gB55>&92)lOGSXt)JQ+pmtUDVqCb3{~L4ND%f zOk%`YwoMx7mVs9nUo?2W-%=mh;%HMY4>4F_j-K5oQNa_gvE^eQ1n1|vH?$dRwkd%ACEbd{ppTpe!P?ri1?>-& z%^gS_stM{@ta2on$jyNn7xOGh^5*Mn&%&*()htf58(0kvc1*@h>4g-Z_gL1V(2PzR z5o$PcYf-n5IL?0GkRNal#jzG=A`buXe}6K#N2Vb3r^I~ z)0X#7$6DK=KW}uM1JBep3^_Fva92erw@2Qv?xu$S`#^nK^x#d2YU-J4*UW!ZvGHw9 z%ZtDDA^$9>W`Dfgid@r^3$1_y+kDvJ7!h^h===Yv?qBU!qZwx%jjIH}o;WC*94l{vU!HB+AOHVJSe=p$A^jqt0S&+^Lzt59@}k zzHLO$-)O{%dt!Mp+=}1b>|Ooe|NJdv0C9V-;B=JbhyA5Q8#0fd5&W?E|I>Vb*&O*VMQ(_(az#rnb%h1b{Z72Y0h?ulK9=9fR#p8Mx4yzBIXZm;pg%omzTSLy zi@#g)!`Mszof?0Ohq=BTIuXg3*kqh*qHqMB+5Ygy!@$4(B|G$@OabvDc=}GzJK@b# z^$?PSQOtL^e>xIS9m$AFU$0u=v1Gmb~LeFCF#^CfwM&VDd48H3Pw$a;ZP5J%Jk7k~8%jnOoVbkEzo%gn>e+yh% zh7`wqbTnS^!~7ihe26LaShq~tI@E45ldW;b_m4XDkjGl*zGL=ohrtTDRP6DY$n#2J z^+O}JPv#!Yn7&sJ5oSL>@SPmLZ6G={z2AU4U38;GiRAfs>|!1aBfJu8C=O#fx)V{j z&L!Yh<4y5ISJC}gM4lDOPrkh6m7new+9nZdp{q}~AhBS9h${VS>{$(Pk z4}r2>B!uy~QoMah~BG5I6rkSl+E z`HnDSM*+gf(-7UoUw(~rjB%nG%bQ~YSFc$r1phJc9!Q~xgg~aOcO+g>>sS1@qsLTi zAI+F!C;Lx^OBfUbY%B%ep%L=#fSj7VzR=ob*0S}MRV_^-?CImD6VoCYXWIwA`}wvb z8Ig<)H++S5zMs98!IewydaUl1qF|*7Mwl6{ffw5C;6mVCDM}@LbttmW#lO1t-r~Pz z#MkMOmLN4iT(1|>)&)c~Eo{oPl7k>l{)rYhTZ`fq7f=6u9w!VeJvNTv-%(l@rxW`L zi$m*^z(x1eKA_%k8u|d~TWfT2aNCjT5f%3V{KC3GSV~)&6aw$_hJ7;cAzEj-yi)(g z@5r@j3&)9)A@7j+U}!pixk;Fn9^NfkVy=((J^Gf7r}kie+P*qMg!M|M>&cZHFWU^G zO<-4kRZ1*0;NaJ7g*9{Q8~J-){%Tt5ONn zE1p+NZ-MKQ{V&K2-mMR_~qO;Z;zFkKEJbJfJ@7Ox; z!k+J87u@UtOn9r`#HL`rX=#4z^6C>NdLVpda`LoJRH{mD{AK-dt1#bmpm*bKNAG0fhYZPS?|{JsdGlA^ zkZHe*Q7J%yX&-St8Qu%RTqaBwyZ5Pwz6;RXuulPHQj}i&z9oJS94oHAwn(_Cu2H99 z=X^Bj;2gn_#;%EH%qW5S6R*u0u~GPYVG8)RO^$l@aFmZm$(t!eYSu54QIoikR?baj z(eDGz`$Xmp>b*iVMdvG;&@(b?Yhv0|`07kn>Z-0O?etO`XP~osiJk1YH0h&lA(l&3 z;LxDOTbh)>gzS@9bQGeFlUr%nS?)O6byBMBCEFsI6-~T>FYgEL^3lS(U z4u}QGtML@G z^;EBQ3mhfRDZig2^D>MpB8s?J*jwb|B8Yv^@WDQ~(0HLlj@ z>S4sw>v{rSRCf2iJ(i=MJ`_0OW(==~VGcz3t#nemAg$_QSkCxx?F;^|cJT0g&9tgk z%@}%z-#Js2#lbJdQ5E7x+L_jd1dNBV!0REcn0UKnYSJ~>8~N(1uZv0C^Wii-y`mynowdA<|LPFkGO=PyH+i_L{lR z8T`GqoGcOs2jy&pvt%8gKAsMS0xf46$? zl@$4`@Lf}CZ_!ID{Tvga1@fyQ{dI6wE_K=A+K36!cc9yqGN%LcH>I#JC7M8z(L2SM zxK*$Sf-bCt^m>}r-4Ll>F5u69bVR~-==&2Wefq1b!_d>P_F($h#ndHGkusDyny&u6LA+pJC2TV2~r5iZgGrOhYxS1%-u)viYh9O2i`zY z`p;MKOz7ZN8GHS?Uhc~Z*V;Z@^%FOhsoXNGF1rtotT`xNQ#0rPQRWhSQUMm;XyHBC zESPa(hVAIf>w>sl*!$qX>|4e3XPOpk~an z+!NUIE<2u(Nh?rtW?1TPe^HmojZqfHT3xV6>zwV8mKbqsr2? z)i>boBFj}C>vv7ji4GqZ4sG{D@UNf?f>d3vhp~gH7*@8iG>JQ&Gnc|?ppuyXaj14_ z_eQCEv`&W)XqJ`vA&UgAQzFcX0r2~PnL%yZbJKC+0W)%Lk1&rtV^=HhmK*+Pk$Wi` zY}a{912gmNJ4aTKme0hDPSnEIl(+|9!2E96vw?pgK- ze=SK9Vb{L=kuo|pOHn=wLLF|-8S+;5iNbodw0sP`XOK6t-8zx(Mh&UL! zPhXz@H4}YIg$aIqvsJV=y4dfLsmhdGJ<}(^RjRr z-xJ7P-p5v>iTWh=v@?jwrVx<5&XT7l-oXw&c<0OATPan=_>)lvs(r6p%R!v(V__qg zye+-;LF#sz;53V)S*3H+xfF7&`V`*%d>JLPS3n*3q{q)DwpD=piyY8?!y7AAO}WyB zuKqB+&w;cG6pq38;Y_gL*VOUu=9_B^4)O|93D*-j$~Vd(1x^lpgV8=!Bmt4-P93It z#dJk+Td&KT3^5#SVmhlA-TDF>jM6K$c38m^aM*ionE5xRv&X0|+^1k;VO|N!zruf^ z?2L4q1*)&OIyHC=>}|3Vx^x)5w>VJ~h#CvM+JY!Y;%2gy>?tMzfj833OY0lZO%??v zBYUgHb1I{_orW{zJoTbb>{s96@Y6{7U#fij2mUO=O<|Pm_1lwgKVhvMvv2{scrnco zbC`FqlyU3T$L)@RL9E=FZFM3q(%euEEl49%4%QET+~-5~nIB3uu;CHDbU@K**Ud<1 zyoVoEWf-yphGIS%aqcl}=;zPmIfS9S+Dd0r`GKkE#$HWkTw&BK;ZQY(ay!Pm+AY7{ z>B&`52M0P7q~0*mP#*AF#oCHI z>^zY73gNqh;^HE4!4a5wl{{-?fk?wyOo#8g7-o_#0=e{5G(O~)U00nw8rtf~>cz`1 z?#yho!G0kSrd0=U<5i#HJsPabxu7D0)jV0bDOd6p3fN)>FjrM6z!2twVB)!66Mu$f zR@<_d2|F>+%S1$Bk=-nVZ(>_OK2#lYC75bE!tPzME>^TP5d9t%rDFF$zSypqWXXIu zHTNEsTdePWL6n2w%=CJjYgRGj&#eB3RVPeO!=f1 zP)$x7PsN?h57In^&=kiV(`XNFb_FHgh}U=^pb2fY-x*qhUYqta=YN=whUX^x- zEUcRjwnVGW*LuPv9p+T#Ydr4*(<@uw(;=p5B+SM1V_%?v!Hi2YDt@w^B%l}FbbP%u6!R{#}2*bURu0Y36VkbFP#v9%eL zb+eBUjkR7h8UxrU>|@+Gvzhuuy7aEAGO*oH$bzg=rnX^ zda`qk#~PT}Y?Lc&Ty2^Q*p9L^KvY*d0loSXft#}8r*AN| zlA=pTpSkg_iYLnHed7qRlk4nwwS#9+SR%2)?B^#HuYy#KEAaVN3c(*LVJvlVLUNJ&d#&KbN=6uMizXWY~;nr6JP!_}qm3&%~Sut&)tJcny?9lE*^ub1dP zjbw!8*F{Tt%3wUns(ea^2{L?5$vx#mmQ?j4!-b0$qVb_Cu3(cYPi<n_3WM8KWI&Yw?9c_B1PNSE27?1ttEKgL zI1)UoUw|rrngXFaM zO+?<35zX!Tk`H6bXCAvNI;=O=3`TY;6Ppd-_E1!VA^rP{RZEkrWh&pd9uqv+EyKt; zB@f|WHZCg2JSBGcl>Q;>YAUX^%F{dBjwcSB$h-NuPXPArOT}YP_PFGS6*%#dj(vEN zUUv^?P!5&An4V<}?S&+!PUDlPh@tzsalZ+7(hdT3QE(K`R;gGy&-HnTT-s`O|4bK} zuj>xR&2;;^GsFpN@|nDFz%|!b*G@EO_dvi8!1#HV?7liy{|&4jYn*D~E|_zM2D#UJ zKN8GY8A8+P>Ug^1q1@muy9SkowPq~t21TVBaUR$RtU&-~d*SMV$5I@}ngJ5WT{5o} z)-u~zu)DN>Aeye$>Xlz0x6{s!uWMZ%y>y=KUK&K$MNS;=y!9&BCtYmD3=}=p<=OA< zH*D~Go~b72NX$%P+DB8};KqX}WX_>n++5fWPsR&ckAehK_GMOsCyGD7Q$&@T=b~00 zqsW)l|Kfl8M8{%=K=sn`-VZ1;JUn>_LUEDHn{9F)2unIw0C5fQFakV?o0(}ZmxrIP zspy{mvxe&kY!B1dcLvLeU)-TR5_(FI1?3|MNgJfwC6x;{3_oGHsBAmJ0*x5KRQ0-o zUdr;+d4ofPx`>VSd_ELJs%nM*9w@;rm{|kMdscc(%$lr#AF-BMAJC%~<_qBP#CvJ9_*AaxiaUAF?8bgfV!$DwJ zPjqY=3Hn`69I@_s6L8fWUX`<60cC7OOWFDmBC1b$a#$VfWNAWAL23){O>5*O@Ig58 zb=D24K7O^%i56PqK?Nz0F`X79G6_ou)kw!ga(?CA66Hlnk zBG1++y<04XQfVWh@VNQCOWuO#34DBef;g(j;;7A}T9|<>+l0TCpL8W@~KSfc3HP14*U2jtrRn#fYZhAI{&RJP$ z_J_^y$ThCAQuxfR9m|Ts@~g8Ni)n0cFtRqMJb-Q$IH_XS*dOOxj&0+EbprQvz6K1E z%k%9UsQ?wFk1mf(KR;p3?RVjfVO$Ic^07Dis!_#$F*oai+;Yv)$GiwLi+gHd0?3#7 z8qZx6<0s_-UxvW4@>XzaeJvhjm{yJ=1soSokMte2Hp$;_f1xpnyKR46-HPGwN7fZw`5LRzbdX(vKJg@^;zKpdOfG|1Kr55fQ1nPBG-_t z2t-sM*Kl2)tLPd^VGx;ynnA91WZnV7Tpf2gz-h?S%`lCn<*?25Tbz2BS5y1iFt3}e z#1;ZKh#l>%?Ap^uZsg<9b~`Xby@G5`IZ7VfUyc%XuXeXkYiF)LaYD$Vn-;_!tb3nK zU+;i7;@zoP@^bAE5E_hAwBr@2+$d**5xIdQ?5f!*Fy~nCN}q$3NU^`osh$BwzEegH zT30=OVjws%?xQ4NOGj4bO%KQGE-EwE#%-oV24~1aW;{b+mq%7-?YE=A+-=7fl;<9v zKCUzv^Qx2qjxP02HK}VzW|efL8dqqtrzY4-ysY?Yz_XlQR2qftT67ER z^9+QFJ69}}ts8p?O>u%e9+d37d&=Z``uRMybZA9&NOj$4mI??U%XB~=?m}Vf)FV`U za}X-+2POeA>EUPEUhX!5%!Xa1BcEF$s>O$4@rAag{tkjgyC1kM9-+v9*NzGaV)S0u zuqL6bCPmE`ET@b7yo;MBXS*q`Jpb_PL(?(KK1m(#Sjqaxo?kG?LIi%o=nw099wg z0<%9E*f`9|M{*HRFD%*YZ9&Q9IOK?-%a!Y9btjl}kHb=n3ARzcS$V@+1R7Ksff)?F zheJ&wFIz3 zXP4vOW;j>YTrBhy2)$MkStSh3nToEu8*3#FdOdYfl_3+9c_Vivc1G0IHnDviD2R`q z8!D6sgVYUD_9}2B>?DlQfDYJ%DFKz_8FFtG^ccEM?QcXb99}?GIuVRQoZq%nsOjxl zZa1ZV)N1m*NQHW(95dk!vMlusf{%3VOSxa@Y7Ub;=Nnh^=Bnen7?Z=FT+1?-w|XmN qio6MVkKp&tGb3kolh=u+_J5VH{ML0G*jL! + var scriptPublisher: PassthroughSubject! + var errorPageExtention: SSLErrorPageTabExtension! + var credentialCreator: MockCredentialCreator! + let errorURLString = "com.example.error" + + override func setUpWithError() throws { + mockWebViewPublisher = PassthroughSubject() + scriptPublisher = PassthroughSubject() + credentialCreator = MockCredentialCreator() + let featureFlagger = MockFeatureFlagger() + errorPageExtention = SSLErrorPageTabExtension(webViewPublisher: mockWebViewPublisher, scriptsPublisher: scriptPublisher, urlCredentialCreator: credentialCreator, featureFlagger: featureFlagger) + } + + override func tearDownWithError() throws { + mockWebViewPublisher = nil + scriptPublisher = nil + errorPageExtention = nil + credentialCreator = nil + } + + func testWhenWebViewPublisherPublishWebViewThenErrorPageExtensionHasCorrectWebView() throws { + // GIVEN + let aWebView = WKWebView() + + // WHEN + mockWebViewPublisher.send(aWebView) + + // THEN + XCTAssertTrue(errorPageExtention.webView === aWebView) + } + + @MainActor func testWhenCertificateExpired_ThenExpectedErrorPageIsShown() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9814, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [], isCurrent: true, isCommitted: true) + let eTldPlus1 = TLD().eTLDplus1(errorURLString) ?? errorURLString + + // WHEN + errorPageExtention.navigation(navigation, didFailWith: error) + + // THEN + let expectedSpecificMessage = SSLErrorType.expired.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() + XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + } + + @MainActor func testWhenCertificateSelfSigned_ThenExpectedErrorPageIsShown() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9807, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [], isCurrent: true, isCommitted: true) + let eTldPlus1 = TLD().eTLDplus1(errorURLString) ?? errorURLString + + // WHEN + errorPageExtention.navigation(navigation, didFailWith: error) + + // THEN + let expectedSpecificMessage = SSLErrorType.selfSigned.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() + XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + } + + @MainActor func testWhenCertificateWrongHost_ThenExpectedErrorPageIsShown() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [], isCurrent: true, isCommitted: true) + let eTldPlus1 = TLD().eTLDplus1(errorURLString) ?? errorURLString + + // WHEN + errorPageExtention.navigation(navigation, didFailWith: error) + + // THEN + let expectedSpecificMessage = SSLErrorType.wrongHost.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() + XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + + } + + @MainActor func test_WhenUserScriptsPublisherPublishSSLErrorPageScript_ThenErrorPageExtensionIsSetAsUserScriptDelegate() { + // GIVEN + let aSSLErrorUserScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: aSSLErrorUserScript) + + // WHEN + scriptPublisher.send(mockScriptProvider) + + // THEN + XCTAssertNotNil(aSSLErrorUserScript.delegate) + } + + @MainActor func testWhenNavigationEnded_IfNoFailure_SSLUserScriptIsNotEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertFalse(userScript.isEnabled) + XCTAssertNil(userScript.failingURL) + } + + @MainActor func testWhenNavigationEnded_IfNonSSLFailure_SSLUserScriptIsNotEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let errorDescription = "some error" + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorUnknown, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!, NSLocalizedDescriptionKey: errorDescription])) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + errorPageExtention.navigation(navigation, didFailWith: error) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertFalse(userScript.isEnabled) + XCTAssertNil(userScript.failingURL) + } + + @MainActor func testWhenNavigationEnded_IfSSLFailure_AndErrorURLIsDifferentFromNavigationURL_SSLUserScriptIsNotEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.different.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + errorPageExtention.navigation(navigation, didFailWith: error) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertFalse(userScript.isEnabled) + XCTAssertEqual(userScript.failingURL?.absoluteString, errorURLString) + } + + @MainActor func testWhenNavigationEnded_IfSSLFailure_AndErrorURLIsTheSameAsNavigationURL_SSLUserScriptIsEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + errorPageExtention.navigation(navigation, didFailWith: error) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertTrue(userScript.isEnabled) + XCTAssertEqual(userScript.failingURL?.absoluteString, errorURLString) + } + + func testWhenLeaveSiteCalled_AndCanGoBackTrue_ThenWebViewGoesBack() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + + // WHEN + errorPageExtention.leaveSite() + + // THEN + XCTAssertTrue(mockWebView.goBackCalled) + } + + func testWhenLeaveSiteCalled_AndCanGoBackFalse_ThenWebViewCloses() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + mockWebView.canGoBack = false + errorPageExtention.webView = mockWebView + + // WHEN + errorPageExtention.leaveSite() + + // THEN + XCTAssertTrue(mockWebView.closedCalled) + } + + func testWhenVisitSiteCalled_ThenWebViewReloads() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + + // WHEN + errorPageExtention.visitSite() + + // THEN + XCTAssertTrue(mockWebView.reloadCalled) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeForCertificateValidation_AndUserRequestBypass_AndNavigationURLIsTheSameAsWevViewURL_ThenReturnsCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.visitSite() + + // WHEN + var disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + if case .credential(let credential) = disposition { + XCTAssertNotNil(credential) + } else { + XCTFail("No credentials found") + } + + // WHEN + disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeNotForCertificateValidation_AndUserRequestBypass_AndNavigationURLIsTheSameAsWevViewURL_ThenReturnsNoCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodClientCertificate) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.visitSite() + + // WHEN + let disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeForCertificateValidation_AndUserDoesNotRequestBypass_AndNavigationURLIsTheSameAsWevViewURL_ThenReturnsNoCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.leaveSite() + + // WHEN + let disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeNotForCertificateValidation_AndUserDoesNotRequestBypass_AndNavigationURLIsNotTheSameAsWevViewURL_ThenReturnsNoCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.different.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.visitSite() + + // WHEN + let disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + +} + +class MockWKWebView: NSObject, ErrorPageTabExtensionNavigationDelegate { + var canGoBack: Bool = true + var url: URL? + var capturedHTML: String = "" + var goBackCalled = false + var reloadCalled = false + var closedCalled = false + + init(url: URL) { + self.url = url + } + + func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) { + capturedHTML = html + } + + func setDocumentHtml(_ html: String) { + capturedHTML = html + } + + func goBack() -> WKNavigation? { + goBackCalled = true + return nil + } + + func reloadPage() -> WKNavigation? { + reloadCalled = true + return nil + } + + func close() { + closedCalled = true + } +} + +class MockSSLErrorPageScriptProvider: SSLErrorPageScriptProvider { + var sslErrorPageUserScript: SSLErrorPageUserScript? + + init(script: SSLErrorPageUserScript?) { + self.sslErrorPageUserScript = script + } +} + +class MockCredentialCreator: URLCredentialCreating { + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + return URLCredential(user: "", password: "", persistence: .forSession) + } +} + +class ChallangeSender: URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} + func isEqual(_ object: Any?) -> Bool { + return false + } + var hash: Int = 0 + var superclass: AnyClass? + func `self`() -> Self { + self + } + func perform(_ aSelector: Selector!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { + return nil + } + func isProxy() -> Bool { + return false + } + func isKind(of aClass: AnyClass) -> Bool { + return false + } + func isMember(of aClass: AnyClass) -> Bool { + return false + } + func conforms(to aProtocol: Protocol) -> Bool { + return false + } + func responds(to aSelector: Selector!) -> Bool { + return false + } + var description: String = "" +} + +class MockFeatureFlagger: FeatureFlagger { + func isFeatureOn(forProvider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding { + return true + } +} diff --git a/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift b/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift new file mode 100644 index 0000000000..f94b23f889 --- /dev/null +++ b/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift @@ -0,0 +1,122 @@ +// +// SSLErrorPageUserScriptTests.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 XCTest +import UserScript + +@testable import DuckDuckGo_Privacy_Browser + +final class SSLErrorPageUserScriptTests: XCTestCase { + + var delegate: CapturingSSLErrorPageUserScriptDelegate! + var userScript: SSLErrorPageUserScript! + + override func setUpWithError() throws { + delegate = CapturingSSLErrorPageUserScriptDelegate() + userScript = SSLErrorPageUserScript() + userScript.delegate = delegate + } + + override func tearDownWithError() throws { + delegate = nil + userScript = nil + } + + func test_FeatureHasCorrectName() throws { + XCTAssertEqual(userScript.featureName, "sslErrorPage") + } + + func test_BrokerIsCorrectlyAdded() throws { + // WHEN + let broker = UserScriptMessageBroker(context: "some contect") + userScript.with(broker: broker) + + // THEN + XCTAssertEqual(userScript.broker, broker) + } + + @MainActor + func test_WhenHandlerForLeaveSiteCalled_AndIsEnabledFalse_ThenNoHandlerIsReturned() { + // WHEN + let handler = userScript.handler(forMethodNamed: "leaveSite") + + // THEN + XCTAssertNil(handler) + } + + @MainActor + func test_WhenHandlerForVisitSiteCalled_AndIsEnabledFalse_ThenNoHandlerIsReturned() { + // WHEN + let handler = userScript.handler(forMethodNamed: "visitSite") + + // THEN + XCTAssertNil(handler) + } + + @MainActor + func test_WhenHandlerForLeaveSiteCalled_AndIsEnabledTrue_ThenLeaveSiteCalled() async { + // GIVEN + var encodable: Encodable? + userScript.isEnabled = true + + // WHEN + let handler = userScript.handler(forMethodNamed: "leaveSite") + if let handler { + encodable = try? await handler(Data(), WKScriptMessage()) + } + + // THEN + XCTAssertNotNil(handler) + XCTAssertNil(encodable) + XCTAssertTrue(delegate.leaveSiteCalled) + XCTAssertFalse(delegate.visitSiteCalled) + } + + @MainActor + func test_WhenHandlerForVisitSiteCalled_AndIsEnabledTrue_ThenVisitSiteCalled() async { + // GIVEN + var encodable: Encodable? + userScript.isEnabled = true + + // WHEN + let handler = userScript.handler(forMethodNamed: "visitSite") + if let handler { + encodable = try? await handler(Data(), WKScriptMessage()) + } + + // THEN + XCTAssertNotNil(handler) + XCTAssertNil(encodable) + XCTAssertTrue(delegate.visitSiteCalled) + XCTAssertFalse(delegate.leaveSiteCalled) + } + +} + +class CapturingSSLErrorPageUserScriptDelegate: SSLErrorPageUserScriptDelegate { + var leaveSiteCalled = false + var visitSiteCalled = false + + func leaveSite() { + leaveSiteCalled = true + } + + func visitSite() { + visitSiteCalled = true + } +} diff --git a/scripts/assets/loc/en.xliff b/scripts/assets/loc/en.xliff new file mode 100644 index 0000000000..36c00babfc --- /dev/null +++ b/scripts/assets/loc/en.xliff @@ -0,0 +1,5106 @@ + + + +
+ +
+ + + DuckDuckGo + DuckDuckGo + Bundle name + + + Allows you to upload photographs and videos + Allows you to upload photographs and videos + Privacy - Camera Usage Description + + + Copyright © 2024 DuckDuckGo. All rights reserved. + Copyright © 2024 DuckDuckGo. All rights reserved. + Copyright (human-readable) + + + Allows you to share your geolocation + Allows you to share your geolocation + Privacy - Location Usage Description + + + Allows you to share your location + Allows you to share your location + Privacy - Location When In Use Usage Description + + + Allows you to share recordings + Allows you to share recordings + Privacy - Microphone Usage Description + + +
+ +
+ +
+ + + + + + + + %@ does not support storing passwords + %@ does not support storing passwords + Data Import disabled checkbox message about a browser (%@) not supporting storing passwords + + + %lld + %lld + + + + %lld tracking attempts blocked + %lld tracking attempts blocked + The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@ + + + **%lld** tracking attempts blocked + **%lld** tracking attempts blocked + The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@ + + + Actual Size + Actual Size + Main Menu View item + + + Add Folder + Add Folder + Add Folder popover: Create folder button + + + Add any details that you think may help us fix the problem + Add any details that you think may help us fix the problem + Data import failure Report dialog suggestion to provide a comments with extra details helping to identify the data import problem. + + + Address + Address + Title of the section of the Identities manager where the user can add/modify an address (street city etc,) + + + Address: + Address: + Add Bookmark dialog bookmark url field heading + + + Always Show + Always Show + Preference for always showing the bookmarks bar + + + Birthday + Birthday + Title of the section of the Identities manager where the user can add/modify a date of birth + + + Bookmark Added + Bookmark Added + Bookmark Added popover title + + + Bookmark import failed. + Bookmark import failed. + Data import summary message of failed bookmarks import. + + + Bookmark import failed: + Bookmark import failed: + Data import summary format of how many bookmarks (%lld) failed to import. + + + Bookmarks Import Complete: + Bookmarks Import Complete: + Bookmarks Data Import result summary headline + + + Bookmarks: + Bookmarks: + Data import summary format of how many bookmarks (%lld) were successfully imported. + + + Bring All to Front + Bring All to Front + Main Menu Window item + + + Capitalize + Capitalize + Main Menu Edit-Transformations item + + + Clear All History… + Clear All History… + Main Menu History item + + + Contact Info + Contact Info + Title of the section of the Identities manager where the user can add/modify contact info (phone, email address) + + + Copy + Copy + Command + + + Country + Country + Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...) + + + Data Detectors + Data Detectors + Main Menu Edit-Substitutions item + + + Delete + Delete + Command + + + Developer + Developer + Main Menu + + + Display progress + Display progress + + + + DuckDuckGo Help + DuckDuckGo Help + Main Menu Help item + + + DuckDuckGo browser version + DuckDuckGo browser version + Data import failure Report dialog description of a report field providing current DuckDuckGo Browser version + + + DuckDuckGo needs your permission to read the %1$@ bookmarks file. Select the %2$@ folder to import bookmarks. + DuckDuckGo needs your permission to read the %1$@ bookmarks file. Select the %2$@ folder to import bookmarks. + Data import warning that DuckDuckGo browser requires file reading permissions for another browser name (%1$@), and instruction to select its (same browser name - %2$@) bookmarks folder. + + + Duplicate Bookmarks Skipped: + Duplicate Bookmarks Skipped: + Data import summary format of how many duplicate bookmarks (%lld) were skipped during import. + + + Edit… + Edit… + Command + + + Enter Full Screen + Enter Full Screen + Main Menu View item + + + Error message & code + Error message & code + Title of the section of a dialog (form where the user can report feedback) where the error message and the error code are shown + + + Favorite This Page… + Favorite This Page… + Main Menu History item + + + Help + Help + Main Menu Help + + + Hide + Hide + Main Menu > View > Home Button > None item + Preferences > Home Button > None item + + + History + History + Main Menu + + + Home + Home + Main Menu View item + + + Home Button + Home Button + Main Menu > View > Home Button item + + + If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password. + +Imported passwords are stored securely using encryption. + If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password. + +Imported passwords are stored securely using encryption. + Warning that Chromium data import would require entering system passwords. + + + Import + Import + Menu item + + + Import Bookmarks + Import Bookmarks + Title of dialog with instruction for the user to import bookmarks from another browser + + + Import Passwords + Import Passwords + Title of dialog with instruction for the user to import passwords from another browser + + + Import Results: + Import Results: + Data Import result summary headline + + + JavaScript Console + JavaScript Console + Main Menu View-Developer item + + + Let’s try doing it manually. It won’t take long. + Let’s try doing it manually. It won’t take long. + Suggestion to switch to a Manual File Data Import when data import fails. + + + Location: + Location: + Add Folder popover: parent folder picker title + + + Make Lower Case + Make Lower Case + Main Menu Edit-Transformations item + + + Make Upper Case + Make Upper Case + Main Menu Edit-Transformations item + + + Manage Bookmarks + Manage Bookmarks + Main Menu History item + + + Merge All Windows + Merge All Windows + Main Menu Window item + + + Minimize + Minimize + Main Menu Window item + + + Never Show + Never Show + Preference for never showing the bookmarks bar on new tab + + + Only Show on New Tab + Only Show on New Tab + Preference for only showing the bookmarks bar on new tab + + + Password import complete. You can now delete the saved passwords file. + Password import complete. You can now delete the saved passwords file. + message about Passwords Data Import completion + + + Password import failed. + Password import failed. + Data import summary message of failed passwords import. + + + Password import failed: + Password import failed: + Data import summary format of how many passwords (%lld) failed to import. + + + Passwords: + Passwords: + Data import summary format of how many passwords (%lld) were successfully imported. + + + Please submit a report to help us fix the issue. + Please submit a report to help us fix the issue. + Data import failure Report dialog title. + + + Recently Closed + Recently Closed + Main Menu History item + + + Reload Page + Reload Page + Main Menu View item + + + Remove + Remove + Remove bookmark button title + + + Reopen All Windows from Last Session + Reopen All Windows from Last Session + Main Menu History item + + + Select %@ Folder… + Select %@ Folder… + + + + Select Profile: + Select Profile: + Browser Profile picker title for Data Import + + + Select data to import: + Select data to import: + Data Import section title for checkboxes of data type to import: Passwords or Bookmarks. + + + Settings… + Settings… + Menu item + + + Show Autofill Shortcut + Show Autofill Shortcut + Main Menu View item + + + Show Bookmarks Shortcut + Show Bookmarks Shortcut + Main Menu View item + + + Show Downloads Shortcut + Show Downloads Shortcut + Main Menu View item + + + Show Left of the Back Button + Show Left of the Back Button + Main Menu > View > Home Button > left position item + + + Show Next Tab + Show Next Tab + Main Menu Window item + + + Show Page Source + Show Page Source + Main Menu View-Developer item + + + Show Previous Tab + Show Previous Tab + Main Menu Window item + + + Show Resources + Show Resources + Main Menu View-Developer item + + + Show Right of the Reload Button + Show Right of the Reload Button + Main Menu > View > Home Button > right position item + + + Show Substitutions + Show Substitutions + Main Menu Edit-Substitutions item + + + Show left of the back button + Show left of the back button + Preferences > Home Button > left position item + + + Show right of the reload button + Show right of the reload button + Preferences > Home Button > right position item + + + Smart Copy/Paste + Smart Copy/Paste + Main Menu Edit-Substitutions item + + + Smart Dashes + Smart Dashes + Main Menu Edit-Substitutions item + + + Smart Links + Smart Links + Main Menu Edit-Substitutions item + + + Smart Quotes + Smart Quotes + Main Menu Edit-Substitutions item + + + Speech + Speech + Main Menu Edit item + + + Start Speaking + Start Speaking + Main Menu Edit-Speech item + + + Stop + Stop + Main Menu View item + + + Stop Speaking + Stop Speaking + Main Menu Edit-Speech item + + + Text Replacement + Text Replacement + Main Menu Edit-Substitutions item + + + That didn’t work either. Please submit a report to help us fix the issue. + That didn’t work either. Please submit a report to help us fix the issue. + Data import failure Report dialog title containing a message that not only automatic data import has failed failed but manual browser data import didn‘t work either. + + + The following information will be sent to DuckDuckGo. No personally identifiable information will be sent. + The following information will be sent to DuckDuckGo. No personally identifiable information will be sent. + Data import failure Report dialog subtitle about the data being collected with the report. + + + The version of the browser you are trying to import from + The version of the browser you are trying to import from + Data import failure Report dialog description of a report field providing version of a browser user is trying to import data from + + + Title: + Title: + Add Bookmark dialog bookmark title field heading + + + Transformations + Transformations + Main Menu Edit item + + + Try importing bookmarks manually instead. + Try importing bookmarks manually instead. + Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file. + + + Try importing passwords manually instead. + Try importing passwords manually instead. + Data import error subtitle: suggestion to import Passwords manually by selecting a CSV or HTML file. + + + View + View + Main Menu View + + + We couldn‘t find any bookmarks. + We couldn‘t find any bookmarks. + Data import error message: Bookmarks weren‘t found. + + + We couldn‘t find any passwords. + We couldn‘t find any passwords. + Data import error message: Passwords weren‘t found. + + + We were unable to import bookmarks directly from %@. + We were unable to import bookmarks directly from %@. + Message when data import fails from a browser. %@ - a browser name + + + We were unable to import passwords directly from %@. + We were unable to import passwords directly from %@. + Message when data import fails from a browser. %@ - a browser name + + + Window + Window + Main Menu + + + You'll be asked to enter your Primary Password for %@. + +Imported passwords are encrypted and only stored on this computer. + You'll be asked to enter your Primary Password for %@. + +Imported passwords are encrypted and only stored on this computer. + Warning that Firefox-based browser name (%@) data import would require entering a Primary Password for the browser. + + + Zoom In + Zoom In + Main Menu View item + + + Zoom Out + Zoom Out + Main Menu View item + + + Continue + Continue + Continue button + + + DuckDuckGo + DuckDuckGo + Application name to be displayed in the About dialog + + + DuckDuckGo for Mac App Store + DuckDuckGo for Mac App Store + Application name to be displayed in the About dialog in App Store app + + + This action cannot be undone. + This action cannot be undone. + Text used in alerts to warn user that a given action cannot be undone + + + Name: + Name: + New bookmark folder dialog folder name field heading + + + Add Favorite + Add Favorite + Button for adding a favorite bookmark + + + Name: + Name: + Add Folder popover: folder name text field title + + + Add Link to Bookmarks + Add Link to Bookmarks + Context menu item + + + Add to Favorites + Add to Favorites + Button for adding bookmarks to favorites + + + Search or enter address + Search or enter address + Empty Address Bar placeholder text displayed on the new tab page. + + + Search DuckDuckGo + Search DuckDuckGo + Suffix of searched terms in address bar. Example: best watching machine . Search DuckDuckGo + + + Visit + Visit + Address bar suffix of possibly visited website. Example: spreadprivacy.com . Visit spreadprivacy.com + + + After installing, return to DuckDuckGo to complete the setup. + After installing, return to DuckDuckGo to complete the setup. + Setup of the integration with Bitwarden app + + + You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up. + You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up. + Description for alert shown when sync bookmarks paused for too many items + + + Bookmarks Sync is Paused + Bookmarks Sync is Paused + Title for alert shown when sync bookmarks paused for too many items + + + You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up. + You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up. + Description for alert shown when sync credentials paused for too many items + + + Passwords Sync is Paused + Passwords Sync is Paused + Title for alert shown when sync credentials paused for too many items + + + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Data syncing unavailable warning message + + + Sync & Backup is Paused + Sync & Backup is Paused + Title of the warning message + + + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Data syncing unavailable warning message + + + A message from %@ + A message from %@ + Title formatted with presenting domain + + + Allow Integration with DuckDuckGo + Allow Integration with DuckDuckGo + Setup of the integration with Bitwarden app + + + Sign In + Sign In + Authentication Alert Sign In Button + + + Sign in to %@. Your login information will be sent securely. + Sign in to %@. Your login information will be sent securely. + Authentication Alert - populated with a domain name + + + Log in to %@. Your password will be sent insecurely because the connection is unencrypted. + Log in to %@. Your password will be sent insecurely because the connection is unencrypted. + Authentication Alert - populated with a domain name + + + Password + Password + Authentication Password field placeholder + + + Authentication Required + Authentication Required + Authentication Alert Title + + + Username + Username + Authentication User name field placeholder + + + Automatically handle cookie pop-ups + Automatically handle cookie pop-ups + Autoconsent settings checkbox title + + + DuckDuckGo will try to select the most private settings available and hide these pop-ups for you. + DuckDuckGo will try to select the most private settings available and hide these pop-ups for you. + Autoconsent feature explanation in settings + + + When we detect cookie pop-ups on sites you visit, we can try to select the most private settings available and hide pop-ups like this. + When we detect cookie pop-ups on sites you visit, we can try to select the most private settings available and hide pop-ups like this. + Body for modal asking the user to auto manage cookies + + + Handle Pop-ups For Me + Handle Pop-ups For Me + Confirm button for modal asking the user to auto manage cookies + + + Want DuckDuckGo to handle cookie pop-ups? + Want DuckDuckGo to handle cookie pop-ups? + Title for modal asking the user to auto manage cookies + + + Want me to handle these for you? I can try to minimize cookies, maximize privacy, and hide pop-ups like these. + Want me to handle these for you? I can try to minimize cookies, maximize privacy, and hide pop-ups like these. + Body for modal asking the user to auto manage cookies + + + Manage Cookie Pop-ups + Manage Cookie Pop-ups + Confirm button for modal asking the user to auto manage cookies + + + No Thanks + No Thanks + Deny button for modal asking the user to auto manage cookies + + + Looks like this site has a cookie pop-up 👇 + Looks like this site has a cookie pop-up 👇 + Title for modal asking the user to auto manage cookies + + + Cookie Pop-ups + Cookie Pop-ups + Autoconsent settings section title + + + Addresses + Addresses + Autofill autosaved data type + + + Save and Autofill + Save and Autofill + Autofill settings section title + + + Receive prompts to save new information and autofill online forms. + Receive prompts to save new information and autofill online forms. + Description of Autofill autosaving feature - used in settings + + + Auto-lock + Auto-lock + Autofill settings section title + + + Also lock password form fill + Also lock password form fill + Lock form filling when auto-lock is active text + + + Copy password + Copy password + Tooltip for the Autofill panel's Copy Password button + + + Copy username + Copy username + Tooltip for the Autofill panel's Copy Username button + + + Excluded Sites + Excluded Sites + Autofill settings section title + + + Websites you selected to never ask to save your password. + Websites you selected to never ask to save your password. + Subtitle providing additional information about the excluded sites section + + + Reset + Reset + Button title allowing users to reset their list of excluded sites + + + If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites. + If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites. + Alert title + + + Reset Excluded Sites? + Reset Excluded Sites? + Alert title + + + Hide password + Hide password + Tooltip for the Autofill panel's Hide Password button + + + Lock autofill after computer is idle for + Lock autofill after computer is idle for + Autofill auto-lock setting + + + Card + Card + Used as placeholder when user iserts a credit card of unknown type (e.g. not Visa, Mastercard) + + + Locked + Locked + Locked status for password manager + + + Unlocked + Unlocked + Unlocked status for password manager + + + Never lock autofill + Never lock autofill + Autofill auto-lock setting + + + If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication. + If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication. + Autofill disabled auto-lock warning + + + Password Manager + Password Manager + Autofill settings section title + + + Bitwarden + Bitwarden + Autofill password manager row title + + + Setup requires installing the Bitwarden app. + Setup requires installing the Bitwarden app. + Autofill password manager Bitwarden disclaimer + + + DuckDuckGo built-in password manager + DuckDuckGo built-in password manager + Autofill password manager row title + + + Payment methods + Payment methods + Autofill autosaved data type + + + View + View + Button to view the recently autosaved password + + + Password saved for %@ + Password saved for %@ + Text confirming a password has been saved for the %@ domain + + + Change in + Change in + Suffix of the label - change in settings - + + + Open %@ + Open %@ + Open password manager button + + + Connected to user %@ + Connected to user %@ + Label describing what user is connected to the password manager + + + You're using %@ to manage passwords + You're using %@ to manage passwords + Explanation of what password manager is being used + + + Settings + Settings + Open Settings Button + + + Show password + Show password + Tooltip for the Autofill panel's Show Password button + + + Usernames and passwords + Usernames and passwords + Autofill autosaved data type + + + View Autofill Content… + View Autofill Content… + View Autofill Content Button name in the autofill settings + + + Bitwarden app found! + Bitwarden app found! + Setup of the integration with Bitwarden app + + + DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager. + DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager. + Requests user Full Disk access in order to access password manager Birwarden + + + All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device. + All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device. + Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device + + + We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo. + We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo. + Description for when the user wants to connect the browser to the password manager Bitwarned. + + + Bitwarden will have access to your browsing history. + Bitwarden will have access to your browsing history. + Warn users that the password Manager Bitwarden will have access to their browsing history + + + Privacy + Privacy + + + + Connect to Bitwarden + Connect to Bitwarden + Title for the Bitwarden onboarding flow + + + Connecting to Bitwarden + Connecting to Bitwarden + It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case we are in the progress of connecting the browser to the Bitwarden password maanger. + + + Unable to find or connect to Bitwarden + Unable to find or connect to Bitwarden + This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue. + + + Handshake not approved in Bitwarden app + Handshake not approved in Bitwarden app + It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action. This message indicates that the handshake process was not approved in the Bitwarden app. + + + Install Bitwarden + Install Bitwarden + Button to install Bitwarden app + + + To begin setup, first install Bitwarden from the App Store. + To begin setup, first install Bitwarden from the App Store. + Setup of the integration with Bitwarden app + + + Bitwarden integration complete! + Bitwarden integration complete! + Setup of the integration with Bitwarden app + + + You are now using Bitwarden as your password manager. + You are now using Bitwarden as your password manager. + Setup of the integration with Bitwarden app + + + Integration with DuckDuckGo is not approved in Bitwarden app + Integration with DuckDuckGo is not approved in Bitwarden app + While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app. + + + Bitwarden is ready to connect to DuckDuckGo! + Bitwarden is ready to connect to DuckDuckGo! + Setup of the integration with Bitwarden app + + + Missing handshake + Missing handshake + While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information). + + + Bitwarden app is not installed + Bitwarden app is not installed + + + + Please update Bitwarden to the latest version + Please update Bitwarden to the latest version + Message that warns user they need to update their password manager Bitwarden app vesion + + + Complete Setup… + Complete Setup… + action option that prompts the user to complete the setup process in Bitwarden preferences + + + Open Bitwarden + Open Bitwarden + Button to open Bitwarden app + + + Bitwarden app not running + Bitwarden app not running + Warns user that the password manager Bitwarden app is not running + + + Unable to find or connect to Bitwarden + Unable to find or connect to Bitwarden + Dialog telling the user Bitwarden (a password manager) is not available + + + Unlock Bitwarden + Unlock Bitwarden + Asks the user to unlock the password manager Bitwarden + + + Waiting for the handshake approval in Bitwarden app + Waiting for the handshake approval in Bitwarden app + While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates the system is waiting for the handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information). + + + Waiting for permission to use Bitwarden in DuckDuckGo… + Waiting for permission to use Bitwarden in DuckDuckGo… + Setup of the integration with Bitwarden app + + + Waiting for the status response from Bitwarden + Waiting for the status response from Bitwarden + It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case that the application is currently waiting for a response from the Bitwarden service. + + + Add + Add + Button to confim a bookmark creation + + + Bookmark Page + Bookmark Page + Context menu item + + + Bookmark This Page + Bookmark This Page + Menu item for bookmarking current page + + + Update Bookmark + Update Bookmark + Option for updating a bookmark + + + Bookmarks + Bookmarks + Button for bookmarks + + + New Bookmark + New Bookmark + Bookmark creation dialog title + + + Copy + Copy + Copy menu item for the bookmarks bar context menu + + + Delete + Delete + Delete menu item for the bookmarks bar context menu + + + Move to End + Move to End + Move to End menu item for the bookmarks bar context menu + + + Empty + Empty + Empty state for a bookmarks bar folder + + + Show Bookmarks Bar + Show Bookmarks Bar + Preference item for showing the bookmarks bar + + + Show + Show + Accept button label on bookmarks bar prompt + + + Hide + Hide + Dismiss button label on bookmarks bar prompt + + + Show the Bookmarks Bar for quick access to your new bookmarks. + Show the Bookmarks Bar for quick access to your new bookmarks. + Message show for bookmarks bar prompt + + + Show Bookmarks Bar? + Show Bookmarks Bar? + Title for bookmarks bar prompt + + + Bookmarks Bar + Bookmarks Bar + Menu item for showing the bookmarks bar + + + Always show + Always show + Preference for always showing the bookmarks bar + + + Only show on New Tab + Only show on New Tab + Preference for only showing the bookmarks bar on new tab + + + Add Bookmark + Add Bookmark + CTA title for adding a Bookmark + + + Add Folder + Add Folder + CTA title for adding a Folder + + + Location + Location + Location field label for Bookmark folder + + + Name + Name + Name field label for Bookmark or Folder + + + URL + URL + URL field label for Bookmar + + + Add Folder + Add Folder + Bookmark folder creation dialog title + + + Edit Folder + Edit Folder + Bookmark folder edit dialog title + + + Add bookmark + Add bookmark + Bookmark creation dialog title + + + Bookmark Added + Bookmark Added + Bookmark added popover title + + + Edit bookmark + Edit bookmark + Bookmark edit dialog title + + + If your bookmarks are saved in another browser, you can import them into DuckDuckGo. + If your bookmarks are saved in another browser, you can import them into DuckDuckGo. + Text displayed in Bookmark Manager when there is no bookmarks yet + + + No bookmarks yet + No bookmarks yet + Title displayed in Bookmark Manager when there is no bookmarks yet + + + Import + Import + Button text to open bookmark import dialog + + + Imported from + Imported from + Name of the folder the imported bookmarks are saved into + + + Mobile bookmarks + Mobile bookmarks + Name of the "Mobile bookmarks" folder imported from other browser + + + Other bookmarks + Other bookmarks + Name of the "Other bookmarks" folder imported from other browser + + + Manage + Manage + Button for opening the bookmarks management interface + + + Manage Bookmarks + Manage Bookmarks + Menu item for opening the bookmarks management interface + + + Open in New Tabs + Open in New Tabs + Open all bookmarks in folder in new tabs + + + Open Bookmarks Panel + Open Bookmarks Panel + Menu item for opening the bookmarks panel + + + Browse without saving local history + Browse without saving local history + Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites. + + + Sign in to a site with a different account + Sign in to a site with a different account + Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites. + + + Troubleshoot websites + Troubleshoot websites + Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites. + + + Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows. + Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows. + This describes the functionality of one of out browser feature Fire Window, highlighting their isolation from other browser data and the automatic deletion of their data upon closure. Additionally, it emphasizes that fire windows offer the same level of tracking protection as other browsing windows. + + + New Fire Tab + New Fire Tab + Tab title for Fire Tab + + + Fire Window + Fire Window + Header shown on the hompage of the Fire Window + + + Cancel + Cancel + Cancel button + + + Cannot Open File + Cannot Open File + Header of the alert dialog informing user it is not possible to open the file + + + The App Store version of DuckDuckGo can only access local files if you drag-and-drop them into a browser window. + + To navigate local files using the address bar, please download DuckDuckGo directly from https://duckduckgo.com/mac. + The App Store version of DuckDuckGo can only access local files if you drag-and-drop them into a browser window. + + To navigate local files using the address bar, please download DuckDuckGo directly from https://duckduckgo.com/mac. + Informative of the alert dialog informing user it is not possible to open the file + + + Check Allow integration with DuckDuckGo. + Check Allow integration with DuckDuckGo. + Setup of the integration with Bitwarden app + + + Check for Update + Check for Update + Button users can use to check for a new update + + + Clear + Clear + Clear button + + + Close Other Tabs + Close Other Tabs + Menu item + + + Close Tab + Close Tab + Menu item + + + Close and Return to Previous Tab + Close and Return to Previous Tab + Close Child Tab on Back Button press and return Back to the Parent Tab without title + + + Close and Return to “%@” + Close and Return to “%@” + Close Child Tab on Back Button press and return Back to the Parent Tab titled “%@” + + + Close Tabs to the Right + Close Tabs to the Right + Menu item + + + Copy + Copy + Copy button + + + Copy + Copy + Copy selection menu item + + + Copy Email Address + Copy Email Address + Context menu item + + + Copy Email Addresses + Copy Email Addresses + Context menu item + + + Copy Image Address + Copy Image Address + Context menu item + + + Click “Send to DuckDuckGo“ to submit report to DuckDuckGo. Crash reports help DuckDuckGo diagnose issues and improve our products. No personal information is sent with this report. + Click “Send to DuckDuckGo“ to submit report to DuckDuckGo. Crash reports help DuckDuckGo diagnose issues and improve our products. No personal information is sent with this report. + Description of the dialog where the user can send a crash report + + + Don’t Send + Don’t Send + Button the user can press to not send the crash report + + + Send to DuckDuckGo + Send to DuckDuckGo + Button the user can press to send the crash report to DuckDuckGo + + + Problem Details + Problem Details + Title of the text field where the problems that caused the crashed are detailed + + + DuckDuckGo Privacy Browser quit unexpectedly. + DuckDuckGo Privacy Browser quit unexpectedly. + Title of the dialog where the user can send a crash report + + + Always allow + Always allow + Privacy Dashboard: Website can always access input media device + + + Always allow on + Always allow on + Permission Popover 'Always allow on' (for domainName) checkbox + + + Ask every time + Ask every time + Privacy Dashboard: Website should always Ask for permission for input media device access + + + Always deny + Always deny + Privacy Dashboard: Website can never access input media device + + + Notify + Notify + Make PopUp Windows always asked from user for current domain + + + Restart your Mac and try again + Restart your Mac and try again + Info to restart macOS after database init failure + + + There was an error initializing the database + There was an error initializing the database + Alert title when we fail to init database + + + Set Default… + Set Default… + represents a prompt message asking the user to make DuckDuckGo their default browser. + + + Make DuckDuckGo your default browser + Make DuckDuckGo your default browser + represents a prompt message asking the user to make DuckDuckGo their default browser. + + + Delete Bookmark + Delete Bookmark + Delete Bookmark button + + + Details + Details + details button + + + Disable + Disable + Email protection Disable button text + + + This will only disable Autofill for Duck Addresses in this browser. + + You can still manually enter Duck Addresses and continue to receive forwarded email. + This will only disable Autofill for Duck Addresses in this browser. + + You can still manually enter Duck Addresses and continue to receive forwarded email. + Message for alert shown when user disables email protection + + + Disable Email Protection Autofill? + Disable Email Protection Autofill? + Title for alert shown when user disables email protection + + + %@ is now Fireproof + %@ is now Fireproof + Domain fireproof status + + + Done + Done + Done button + + + Don’t Quit + Don’t Quit + Don’t Quit button + + + Don't Save + Don't Save + Don't Save button + + + Don't Update + Don't Update + Don't Update button + + + Finishing download… + Finishing download… + Download being finished information text + + + Download Linked File As… + Download Linked File As… + Context menu item + + + Starting download… + Starting download… + Download being initiated information text + + + , and other files + , and other files + Alert text format element for “, and other files” + + + Are you sure you want to quit? DuckDuckGo Privacy Browser is currently downloading “%1$@”%2$@. If you quit now DuckDuckGo Privacy Browser won’t finish downloading this file. + Are you sure you want to quit? DuckDuckGo Privacy Browser is currently downloading “%1$@”%2$@. If you quit now DuckDuckGo Privacy Browser won’t finish downloading this file. + Alert text format when trying to quit application while file “filename”[, and others] are being downloaded + + + A download is in progress. + A download is in progress. + Alert title when trying to quit application while files are being downloaded + + + Always ask where to save files + Always ask where to save files + Downloads preferences checkbox + + + %1$@ of %2$@ + %1$@ of %2$@ + Number of bytes out of total bytes downloaded (1Mb of 2Mb) + + + Change… + Change… + Change downloads directory button + + + Clear All + Clear All + Contextual menu item in downloads manager to clear all downloaded items from the list + + + Copy Download Link + Copy Download Link + Contextual menu item in downloads manager to copy the downloaded link + + + Downloads + Downloads + Title of the dialog that manages the Downloads in the browser + + + Canceled + Canceled + Short error description when downloaded file download was canceled + + + Could not move file to Downloads + Could not move file to Downloads + Short error description when could not move downloaded file to the Downloads folder + + + Error + Error + Short error description when Download failed + + + Removed + Removed + Short error description when downloaded file removed from Downloads folder + + + Location + Location + Downloads directory location + + + No recent downloads + No recent downloads + Label in the downloads manager that shows that there are no recently downloaded items + + + Open Downloads Folder + Open Downloads Folder + Button in the downloads manager that allows the user to open the downloads folder + + + Open Originating Website + Open Originating Website + Contextual menu item in downloads manager to open the downloaded file originating website + + + Open + Open + Contextual menu item in downloads manager to open the downloaded file + + + Automatically open the Downloads panel when downloads complete + Automatically open the Downloads panel when downloads complete + Checkbox to open a Download Manager popover when downloads are completed + + + Remove from List + Remove from List + Contextual menu item in downloads manager to remove the given downloaded from the list of downloaded files + + + Stop + Stop + Contextual menu item in downloads manager to restart the download + + + Show in Finder + Show in Finder + Contextual menu item in downloads manager to show the downloaded file in Finder + + + %@/s + %@/s + Download speed format (1Mb/sec) + + + Stop + Stop + Contextual menu item in downloads manager to stop the download + + + Cancel Download + Cancel Download + Mouse-over tooltip for Cancel Download button + + + Download Again + Download Again + Mouse-over tooltip for Download [deleted file] Again button + + + Restart Download + Restart Download + Mouse-over tooltip for Restart Download button + + + Show in Finder + Show in Finder + Mouse-over tooltip for Show in Finder button + + + Always open YouTube videos in Duck Player + Always open YouTube videos in Duck Player + Private YouTube Player option + + + Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations. + Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations. + Private YouTube Player explanation in settings + + + Never use Duck Player + Never use Duck Player + Private YouTube Player option + + + Show option to use Duck Player over YouTube previews on hover + Show option to use Duck Player over YouTube previews on hover + Private YouTube Player option + + + Duck Player + Duck Player + Private YouTube Player settings title + + + Duplicate Tab + Duplicate Tab + Menu item. Duplicate as a verb + + + Edit + Edit + Edit button + + + Edit Favorite + Edit Favorite + Header of the view that edits a favorite bookmark + + + Edit Folder + Edit Folder + Header of the view that edits a bookmark folder + + + New address copied to your clipboard + New address copied to your clipboard + Notification that the Private email address was copied to clipboard after the user generated a new address + + + Email Protection + Email Protection + Menu item email feature + + + Generate Private Duck Address + Generate Private Duck Address + Create an email alias sub menu item + + + Manage Account + Manage Account + Manage private email account sub menu item + + + Disable Email Protection Autofill + Disable Email Protection Autofill + Disable email sub menu item + + + Enable Email Protection + Enable Email Protection + Sub menu item to enable Email Protection + + + An unknown error has occurred + An unknown error has occurred + Generic error message on a dialog for when the cause is not known. + + + Please check that no file exists at the location you selected. + Please check that no file exists at the location you selected. + Alert message when exporting bookmarks fails + + + Failed to Export Bookmarks… + Failed to Export Bookmarks… + Alert title when exporting login data fails + + + Bookmarks + Bookmarks + The last part of the suggested file for exporting bookmarks + + + Export Bookmarks… + Export Bookmarks… + Export bookmarks menu item + + + Export Passwords… + Export Passwords… + Opens Export Logins Data dialog + + + Please check that no file exists at the location you selected. + Please check that no file exists at the location you selected. + Alert message when exporting login data fails + + + Failed to Export Passwords + Failed to Export Passwords + Alert title when exporting login data fails + + + Passwords + Passwords + The last part of the suggested file name for exporting logins + + + This file contains your passwords in plain text and should be saved in a secure location and deleted when you are done. +Anyone with access to this file will be able to read your passwords. + This file contains your passwords in plain text and should be saved in a secure location and deleted when you are done. +Anyone with access to this file will be able to read your passwords. + Warning text presented when exporting logins. + + + Favorites + Favorites + Title text for the Favorites menu item + + + Please describe the problem in as much detail as possible: + Please describe the problem in as much detail as possible: + Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo + + + Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version. + Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version. + Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo + + + What feature would you like to see? + What feature would you like to see? + Label in the feedback form for feature requests. + + + Please give us your feedback: + Please give us your feedback: + Label in the feedback form + + + Find in page + Find in page + Placeholder text for the text field where the user inputs strings to searcg in the web page + + + %1$d of %2$d + %1$d of %2$d + Find in page status (e.g. 1 of 99) + + + Find in Page… + Find in Page… + Menu item title + + + Close active tabs (%1$d) and clear all browsing history and cookies (sites: %2$d). + Close active tabs (%1$d) and clear all browsing history and cookies (sites: %2$d). + Info in the Fire Button popover + + + Clear all tabs and related site data + Clear all tabs and related site data + Description of the 'All Data' configuration option for the fire button + + + All sites + All sites + Configuration option for fire button + + + Clear current window and related site data + Clear current window and related site data + Description of the 'Current Window' configuration option for the fire button + + + All sites visited in current tab + All sites visited in current tab + Configuration option for fire button + + + All sites visited in current window + All sites visited in current window + Configuration option for fire button + + + All windows will close + All windows will close + Warning label shown in an expanded view of the fire popover + + + Selected sites will be cleared + Selected sites will be cleared + Category of domains in fire button dialog + + + Close and Burn This Window + Close and Burn This Window + Button that allows the user to close and burn the browser burner window + + + Deleting browsing data… + Deleting browsing data… + Text shown in dialog while removing browsing data + + + Details + Details + Button to show more details + + + Close Tabs and Clear Data + Close Tabs and Clear Data + Title of the dialog where the user can close browser tabs and clear data. + + + An isolated window that doesn’t save any data + An isolated window that doesn’t save any data + Explanation of what a fire window is. + + + Open New Fire Window + Open New Fire Window + Title of the part of the dialog where the user can open a fire window. + + + Fireproof sites won't be cleared + Fireproof sites won't be cleared + Category of domains in fire button dialog + + + Current tab will close + Current tab will close + Warning label shown in an expanded view of the fire popover + + + Pinned tab will reload + Pinned tab will reload + Warning label shown in an expanded view of the fire popover + + + Current window will close + Current window will close + Warning label shown in an expanded view of the fire popover + + + Data, browsing history, and cookies can build up in your browser over time. Use the Fire Button to clear it all away. + Data, browsing history, and cookies can build up in your browser over time. Use the Fire Button to clear it all away. + Description in the dialog that explains the Fire feature. + + + Leave No Trace + Leave No Trace + Title of the dialog that explains the Fire feature. + + + Close this tab and clear its browsing history and cookies (sites: %d). + Close this tab and clear its browsing history and cookies (sites: %d). + Info in the Fire Button popover + + + Select a site to clear its data. + Select a site to clear its data. + Info label in the fire button popover + + + Clear data only for selected domains + Clear data only for selected domains + Description of the 'Current Window' configuration option for the fire button + + + Fireproof + Fireproof + Fireproof button + + + Ask to Fireproof websites when signing in + Ask to Fireproof websites when signing in + Fireproof settings checkbox title + + + Fireproofing this site will keep you signed in after using the Fire Button. + Fireproofing this site will keep you signed in after using the Fire Button. + Fireproof confirmation message + + + Would you like to Fireproof %@? + Would you like to Fireproof %@? + Fireproof confirmation title + + + Remove All + Remove All + Label of a button that allows the user to remove all the websites from the fireproofed list + + + When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button. + When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button. + Fireproofing mechanism explanation + + + Manage Fireproof Sites… + Manage Fireproof Sites… + Fireproof settings button caption + + + Fireproof Sites + Fireproof Sites + Fireproof sites list title + + + Add + Add + Button to confim a bookmark folder creation + + + Delete Folder + Delete Folder + Option for deleting a folder + + + New Folder + New Folder + Option for creating a new folder + + + Rename Folder + Rename Folder + Option for renaming a folder + + + Discard + Discard + Label of a button that allows the user discard an action/change + + + Remove + Remove + Label of a button that allows the user to remove an item + + + Got It! + Got It! + Got it button + + + Enable Global Privacy Control + Enable Global Privacy Control + GPC settings checkbox title + + + Tells participating websites not to sell or share your data. + Tells participating websites not to sell or share your data. + GPC explanation in settings + + + Global Privacy Control (GPC) + Global Privacy Control (GPC) + GPC settings title + + + Cookies and site data for all sites will also be cleared, unless the site is Fireproof. + Cookies and site data for all sites will also be cleared, unless the site is Fireproof. + Description in the alert with the confirmation to clear all data + + + Clear all history and +close all tabs? + Clear all history and +close all tabs? + Alert with the confirmation to clear all history and data + + + Cookies and other data for sites visited on this day will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Cookies and other data for sites visited on this day will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Description in the alert with the confirmation to clear browsing history + + + Clear History for %@? + Clear History for %@? + Alert with the confirmation to clear all data + + + Cookies and other data for sites visited today will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Cookies and other data for sites visited today will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Description in the alert with the confirmation to clear browsing history + + + Clear history for today +and close all tabs? + Clear history for today +and close all tabs? + Alert with the confirmation to clear all data + + + Clear This History… + Clear This History… + Menu item to clear parts of history and data + + + Older… + Older… + Menu item representing older history + + + Recently Visited + Recently Visited + Section header of the history menu + + + History will be cleared for this site, but related data will remain, because this site is Fireproof + History will be cleared for this site, but related data will remain, because this site is Fireproof + Message for an alert displayed when trying to burn a fireproof website + + + Clear History + Clear History + Button caption for the burn fireproof website alert + + + Keep browsing to see how many trackers were blocked + Keep browsing to see how many trackers were blocked + This string represents the message for an empty state item on the home page, encouraging the user to keep browsing to see how many trackers were blocked + + + Recently visited sites appear here + Recently visited sites appear here + This string represents the title for an empty state item on the home page, indicating that recently visited sites will appear here + + + No trackers blocked + No trackers blocked + This string represents a message on the home page indicating that no trackers were blocked + + + No trackers found + No trackers found + This string represents a message on the home page indicating that no trackers were found + + + PAST 7 DAYS + PAST 7 DAYS + Past 7 days in uppercase. + + + No recent activity + No recent activity + This string represents a message in the protection summary on the home page, indicating that there is no recent activity + + + %@ tracking attempts blocked + %@ tracking attempts blocked + The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@ + + + Bookmarks + Bookmarks + Title text for the Bookmarks import option + + + HTML Bookmarks File (for other browsers) + HTML Bookmarks File (for other browsers) + Title text for the HTML Bookmarks importer + + + Importing bookmarks… + Importing bookmarks… + Operation progress info message about indefinite number of bookmarks being imported + + + Importing bookmarks (%d)… + Importing bookmarks (%d)… + Operation progress info message about %d number of bookmarks being imported + + + Select Safari Folder… + Select Safari Folder… + Text for the Safari data import permission button + + + Select Bookmarks HTML File… + Select Bookmarks HTML File… + Button text for selecting HTML Bookmarks file + + + Import Browser Data + Import Browser Data + Import Browser Data dialog title + + + Import Bookmarks… + Import Bookmarks… + Opens Import Browser Data dialog + + + Import Passwords… + Import Passwords… + Opens Import Browser Data dialog + + + %1$d Open and unlock **%2$s** +%3$d Select **File → Export vault** from the Menu Bar +%4$d Select the File Format: **.csv** +%5$d Enter your Bitwarden master password +%6$d Click %7$@ and save the file someplace you can find it (e.g., Desktop) +%8$d %9$@ + %1$d Open and unlock **%2$s** +%3$d Select **File → Export vault** from the Menu Bar +%4$d Select the File Format: **.csv** +%5$d Enter your Bitwarden master password +%6$d Click %7$@ and save the file someplace you can find it (e.g., Desktop) +%8$d %9$@ + Instructions to import Passwords as CSV from Bitwarden. +%2$s - app name (Bitwarden) +%7$@ - hamburger menu icon +%9$@ - “Select Bitwarden CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Password Manager** +%5$d Click %6$@ **at the top left** of the Password Manager and select **Settings** +%7$d Find “Export Passwords” and click **Download File** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Password Manager** +%5$d Click %6$@ **at the top left** of the Password Manager and select **Settings** +%7$d Find “Export Passwords” and click **Download File** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + Instructions to import Passwords as CSV from Brave browser. +%N$d - step number +%2$s - browser name (Brave) +%4$@, %6$@ - hamburger menu icon +%10$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Google Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Google Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Google Chrome browser. +%N$d - step number +%2$s - browser name (Chrome) +%4$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Chromium-based browsers. +%N$d - step number +%2$s - browser name +%4$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Type “_coccoc://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Type “_coccoc://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Cốc Cốc browser. +%N$d - step number +%2$s - browser name (Cốc Cốc) +%5$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords** +%5$d Click %6$@ then **Export Logins…** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords** +%5$d Click %6$@ then **Export Logins…** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + Instructions to import Passwords as CSV from Firefox. +%N$d - step number +%2$s - browser name (Firefox) +%4$@, %6$@ - hamburger menu icon +%9$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + The CSV importer will try to match column headers to their position. +If there is no header, it supports two formats: +%1$d URL, Username, Password +%2$d Title, URL, Username, Password +%3$@ + The CSV importer will try to match column headers to their position. +If there is no header, it supports two formats: +%1$d URL, Username, Password +%2$d Title, URL, Username, Password +%3$@ + Instructions to import a generic CSV passwords file. +%N$d - step number +%3$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Click on the **%2$s** icon in your browser and enter your master password +%3$d Select **Open My Vault** +%4$d From the sidebar select **Advanced Options → Export** +%5$d Enter your LastPass master password +%6$d Select the File Format: **Comma Delimited Text (.csv)** +%7$d %8$@ + %1$d Click on the **%2$s** icon in your browser and enter your master password +%3$d Select **Open My Vault** +%4$d From the sidebar select **Advanced Options → Export** +%5$d Enter your LastPass master password +%6$d Select the File Format: **Comma Delimited Text (.csv)** +%7$d %8$@ + Instructions to import Passwords as CSV from LastPass. +%2$s - app name (LastPass) +%8$@ - “Select LastPass CSV File” button +**bold text**; _italic text_ + + + %1$d Open and unlock **%2$s** +%3$d Select the vault you want to export (you can only export one vault at a time) +%4$d Select **File → Export → All Items** from the Menu Bar +%5$d Enter your 1Password master or account password +%6$d Select the File Format: **iCloud Keychain (.csv)** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + %1$d Open and unlock **%2$s** +%3$d Select the vault you want to export (you can only export one vault at a time) +%4$d Select **File → Export → All Items** from the Menu Bar +%5$d Enter your 1Password master or account password +%6$d Select the File Format: **iCloud Keychain (.csv)** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + Instructions to import Passwords as CSV from 1Password 7. +%2$s - app name (1Password) +%9$@ - “Select 1Password CSV File” button +**bold text**; _italic text_ + + + %1$d Open and unlock **%2$s** +%3$d Select **File → Export** from the Menu Bar and choose the account you want to export +%4$d Enter your 1Password account password +%5$d Select the File Format: **CSV (Logins and Passwords only)** +%6$d Click Export Data and save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open and unlock **%2$s** +%3$d Select **File → Export** from the Menu Bar and choose the account you want to export +%4$d Enter your 1Password account password +%5$d Select the File Format: **CSV (Logins and Passwords only)** +%6$d Click Export Data and save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from 1Password 8. +%2$s - app name (1Password) +%8$@ - “Select 1Password CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Select **Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Select **Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Opera browser. +%N$d - step number +%2$s - browser name (Opera) +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Opera GX browsers. +%N$d - step number +%2$s - browser name (Opera GX) +%5$@ - menu button icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **Safari** +%2$d Select **File → Export → Passwords** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + %1$d Open **Safari** +%2$d Select **File → Export → Passwords** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + Instructions to import Passwords as CSV from Safari. +%N$d - step number +%5$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Type “_chrome://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Type “_chrome://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords exported as CSV from Vivaldi browser. +%N$d - step number +%2$s - browser name (Vivaldi) +%5$@ - menu button icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords and cards** +%5$d Click %6$@ then **Export passwords** +%7$d Choose **To a text file (not secure)** and click **Export** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords and cards** +%5$d Click %6$@ then **Export passwords** +%7$d Choose **To a text file (not secure)** and click **Export** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + Instructions to import Passwords as CSV from Yandex Browser. +%N$d - step number +%2$s - browser name (Yandex) +%4$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + Cancel + Cancel + Cancel button for data import alerts + + + Import + Import + Import button for data import alerts + + + Done + Done + Button text for finishing the data import + + + Import + Import + Button text for importing data + + + Manual import… + Manual import… + Button text for initiating manual data import using a HTML or CSV file when automatic import has failed + + + DuckDuckGo won't save or share your %1$@ Primary Password, but DuckDuckGo needs it to access and import passwords from %1$@. + DuckDuckGo won't save or share your %1$@ Primary Password, but DuckDuckGo needs it to access and import passwords from %1$@. + Alert body text when the data import needs a password + + + Enter Primary Password for %@ + Enter Primary Password for %@ + Alert title text when the data import needs a password + + + Skip + Skip + Button text to skip an import step + + + Skip bookmarks + Skip bookmarks + Button text to skip bookmarks manual import + + + Skip passwords + Skip passwords + Button text to skip bookmarks manual import + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmark Manager** +%4$d Click %5$@ then **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmark Manager** +%4$d Click %5$@ then **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Chromium-based browsers. +%N$d - step number +%2$s - browser name +%5$@ - hamburger menu icon +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Manage Bookmarks** +%4$d Click %5$@ then **Export bookmarks to HTML…** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Manage Bookmarks** +%4$d Click %5$@ then **Export bookmarks to HTML…** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Firefox based browsers. +%N$d - step number +%2$s - browser name (Firefox) +%5$@ - hamburger menu icon +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open your old browser +%2$d Open **Bookmark Manager** +%3$d Export bookmarks to HTML… +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + %1$d Open your old browser +%2$d Open **Bookmark Manager** +%3$d Export bookmarks to HTML… +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + Instructions to import a generic HTML Bookmarks file. +%N$d - step number +%6$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Open full Bookmarks view…** in the bottom left +%5$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Open full Bookmarks view…** in the bottom left +%5$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Opera browser. +%N$d - step number +%2$s - browser name (Opera) +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%5$d Save the file someplace you can find it (e.g., Desktop) +%6$d %7$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%5$d Save the file someplace you can find it (e.g., Desktop) +%6$d %7$@ + Instructions to import Bookmarks exported as HTML from Opera GX browser. +%N$d - step number +%2$s - browser name (Opera GX) +%7$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **Safari** +%2$d Select **File → Export → Bookmarks** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + %1$d Open **Safari** +%2$d Select **File → Export → Bookmarks** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + Instructions to import Bookmarks exported as HTML from Safari. +%N$d - step number +%5$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **File → Export Bookmarks…** +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **File → Export Bookmarks…** +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + Instructions to import Bookmarks exported as HTML from Vivaldi browser. +%N$d - step number +%2$s - browser name (Vivaldi) +%6$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Favorites → Bookmark Manager** +%4$d Click %5$@ then **Export bookmarks to HTML file** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Favorites → Bookmark Manager** +%4$d Click %5$@ then **Export bookmarks to HTML file** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Yandex Browser. +%N$d - step number +%2$s - browser name (Yandex) +%5$@ - hamburger menu icon +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + CSV Passwords File (for other browsers) + CSV Passwords File (for other browsers) + Title text for the CSV importer + + + Passwords + Passwords + Title text for the Passwords import option + + + Select Passwords CSV File… + Select Passwords CSV File… + Button text for selecting a CSV file + + + Select %@ CSV File… + Select %@ CSV File… + Button text for selecting a CSV file exported from (LastPass or Bitwarden or 1Password - %@) + + + You can find your version by selecting **%1$s → About %2$s** from the Menu Bar. + You can find your version by selecting **%1$s → About %2$s** from the Menu Bar. + Instructions how to find an installed 1Password password manager app version. +%1$s, %2$s - app name (1Password) + + + Importing passwords… + Importing passwords… + Operation progress info message about indefinite number of passwords being imported + + + Importing passwords (%d)… + Importing passwords (%d)… + Operation progress info message about %d number of passwords being imported + + + Get Started + Get Started + Get Started button on an invite dialog + + + We didn’t recognize this Invite Code. + We didn’t recognize this Invite Code. + Message to show after user enters an unrecognized invite code + + + Learn More + Learn More + Learn More link + + + Bitwarden not installed… + Bitwarden not installed… + Setup of the integration with Bitwarden app + + + macOS version + macOS version + Data import failure Report dialog description of a report field providing user‘s macOS version + + + Check for Updates… + Check for Updates… + Main Menu DuckDuckGo item + + + Hide DuckDuckGo + Hide DuckDuckGo + Main Menu DuckDuckGo item + + + Hide Others + Hide Others + Main Menu DuckDuckGo item + + + Preferences… + Preferences… + Main Menu DuckDuckGo item + + + Quit DuckDuckGo + Quit DuckDuckGo + Main Menu DuckDuckGo item + + + Services + Services + Main Menu DuckDuckGo item + + + Show All + Show All + Main Menu DuckDuckGo item + + + Edit + Edit + Main Menu Edit + + + Copy + Copy + Main Menu Edit item + + + Cut + Cut + Main Menu Edit item + + + Delete + Delete + Main Menu Edit item + + + Spelling and Grammar + Spelling and Grammar + Main Menu Edit item + + + Find + Find + Main Menu Edit item + + + Find Next + Find Next + Main Menu Edit-Find item + + + Find Previous + Find Previous + Main Menu Edit-Find item + + + Hide Find + Hide Find + Main Menu Edit-Find item + + + Paste + Paste + Main Menu Edit item + + + Paste and Match Style + Paste and Match Style + Main Menu Edit item - Action that allows the user to paste copy into a target document and the target document's style will be retained (instead of the source style) + + + Redo + Redo + Main Menu Edit item + + + Select All + Select All + Main Menu Edit item + + + Check Document Now + Check Document Now + Main Menu Edit-Spellingand item + + + Check Grammar With Spelling + Check Grammar With Spelling + Main Menu Edit-Spellingand item + + + Check Spelling While Typing + Check Spelling While Typing + Main Menu Edit-Spellingand item + + + Correct Spelling Automatically + Correct Spelling Automatically + Main Menu Edit-Spellingand item + + + Show Spelling and Grammar + Show Spelling and Grammar + Main Menu Edit-Spellingand item + + + Substitutions + Substitutions + Main Menu Edit item + + + Undo + Undo + Main Menu Edit item + + + File + File + Main Menu File + + + Close All Windows + Close All Windows + Main Menu File item + + + Close Window + Close Window + Main Menu File item + + + Export + Export + Main Menu File item + + + Bookmarks… + Bookmarks… + Main Menu File-Export item + + + Passwords… + Passwords… + Main Menu File-Export item + + + Import Bookmarks and Passwords… + Import Bookmarks and Passwords… + Main Menu File item + + + New Tab + New Tab + Main Menu File item + + + Open Location… + Open Location… + Main Menu File item- Menu option that allows the user to connect to an address (type an address) on click the address bar of the browser is selected and the user can type. + + + Save As… + Save As… + Main Menu File item + + + Hide Downloads + Hide Downloads + Hide Downloads Popover + + + Close Developer Tools + Close Developer Tools + Hide Web Inspector/Close Developer Tools + + + Show Downloads + Show Downloads + Show Downloads Popover + + + Open Developer Tools + Open Developer Tools + Show Web Inspector/Open Developer Tools + + + Add Folder… + Add Folder… + Menu item to add a folder + + + Edit… + Edit… + Menu item to edit a bookmark or a folder + + + New Tab + New Tab + Context menu item + + + Change Default Page Zoom… + Change Default Page Zoom… + Default page zoom picker title + + + Show Less + Show Less + For collapsing views to show less. + + + Show More + Show More + For expanding views to show more. + + + Mute Tab + Mute Tab + Menu item. Mute tab + + + Window with multiple tabs (%d) + Window with multiple tabs (%d) + String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window + + + Back + Back + Context menu item + + + Forward + Forward + Context menu item + + + Never Ask for This Site + Never Ask for This Site + Never ask to save login credentials for this site button + + + New Fire Window + New Fire Window + Menu item title + + + New Window + New Window + Menu item title + + + New Tab Page + New Tab Page + Title of the popover that appears when pressing the bottom right button + + + Favorites + Favorites + Title of the Favorites section in the home page + + + Show Next Steps + Show Next Steps + Title of the menu item in the home page to show/hide continue setup section + + + Show Favorites + Show Favorites + Title of the menu item in the home page to show/hide favorite section + + + Show Recent Activity + Show Recent Activity + Title of the menu item in the home page to show/hide recent activity section + + + Recent Activity + Recent Activity + Title of the RecentActivity section in the home page + + + Import Now + Import Now + Action title on the action menu of the Import card of the Set Up section in the home page + + + Make Default Browser + Make Default Browser + Action title on the action menu of the Default Browser card + + + We automatically block trackers as you browse. It's privacy, simplified. + We automatically block trackers as you browse. It's privacy, simplified. + Summary of the Default Browser card + + + Default to Privacy + Default to Privacy + Title of the Default Browser card of the Set Up section in the home page + + + Try Duck Player + Try Duck Player + Action title on the action menu of the Duck Player card of the Set Up section in the home page + + + Enjoy a clean viewing experience without personalized ads. + Enjoy a clean viewing experience without personalized ads. + Summary of the Duck Player card of the Set Up section in the home page + + + Clean Up YouTube + Clean Up YouTube + Title of the Duck Player card of the Set Up section in the home page + + + Get a Duck Address + Get a Duck Address + Action title on the action menu of the Email Protection card of the Set Up section in the home page + + + Generate custom @duck.com addresses that clean trackers from incoming email. + Generate custom @duck.com addresses that clean trackers from incoming email. + Summary of the Email Protection card of the Set Up section in the home page + + + Protect Your Inbox + Protect Your Inbox + Title of the Email Protection card of the Set Up section in the home page + + + Import bookmarks, favorites, and passwords from your old browser. + Import bookmarks, favorites, and passwords from your old browser. + Summary of the Import card of the Set Up section in the home page + + + Bring Your Stuff + Bring Your Stuff + Title of the Import card of the Set Up section in the home page + + + Dismiss + Dismiss + Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item + + + Next Steps + Next Steps + Title of the setup section in the home page + + + Share Your Thoughts + Share Your Thoughts + Action title of the Day 0 durvey of the Set Up section in the home page + + + Take our short survey and help us build the best browser. + Take our short survey and help us build the best browser. + Summary of the Day 0 durvey of the Set Up section in the home page + + + Tell Us What Brought You Here + Tell Us What Brought You Here + Title of the Day 0 durvey of the Set Up section in the home page + + + Share Your Thoughts + Share Your Thoughts + Action title of the Day 7 durvey of the Set Up section in the home page + + + Take our short survey and help us build the best browser. + Take our short survey and help us build the best browser. + Summary of the Day 7 durvey of the Set Up section in the home page + + + Help Us Improve + Help Us Improve + Title of the Day 7 durvey of the Set Up section in the home page + + + Next + Next + Next button + + + DuckDuckGo needs permission to access your Downloads folder + DuckDuckGo needs permission to access your Downloads folder + Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder + + + Grant access in Security & Privacy preferences in System Settings. + Grant access in Security & Privacy preferences in System Settings. + Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 12 and below + + + Grant access in Privacy & Security preferences in System Settings. + Grant access in Privacy & Security preferences in System Settings. + Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 13 and above + + + Grant access to the location of download. + Grant access to the location of download. + Alert presented to user if the app doesn't have rights to access selected folder + + + DuckDuckGo needs permission to access selected folder + DuckDuckGo needs permission to access selected folder + Header of the alert dialog informing user about failed download + + + Cookies Managed + Cookies Managed + Notification that appears when browser automatically handle cookies + + + Pop-up Hidden + Pop-up Hidden + Notification that appears when browser cosmetically hides a cookie popup + + + Not Now + Not Now + Not Now button + + + OK + OK + OK button + + + Import + Import + Launch the import data UI + + + First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers. + First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers. + Call to action to import data from other browsers + + + Maybe Later + Maybe Later + Skip a step of the onboarding flow + + + Let's Do It! + Let's Do It! + Launch the set default UI + + + Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time. + Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time. + Call to action to set the browser as default + + + You’re all set! + +Want to see how I protect you? Try visiting one of your favorite sites 👆 + +Keep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒 + You’re all set! + +Want to see how I protect you? Try visiting one of your favorite sites 👆 + +Keep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒 + Call to action to start using the app as a browser + + + Get Started + Get Started + Start the onboarding flow + + + Tired of being tracked online? You've come to the right place 👍 + +I'll help you stay private️ as you search and browse the web. Trackers be gone! + Tired of being tracked online? You've come to the right place 👍 + +I'll help you stay private️ as you search and browse the web. Trackers be gone! + Detailed welcome to the app text + + + Welcome to DuckDuckGo! + Welcome to DuckDuckGo! + General welcome to the app title + + + Open + Open + Open button + + + Open All in New Tabs + Open All in New Tabs + Menu item that opens all the bookmarks in a folder to new tabs + + + Open All in New Window + Open All in New Window + Menu item that opens all the bookmarks in a folder in a new window + + + Open Bitwarden + Open Bitwarden + Button to open Bitwarden app + + + Open Bitwarden and Log in or Unlock your vault. + Open Bitwarden and Log in or Unlock your vault. + Setup of the integration with Bitwarden app + + + The app required to open that link can’t be found + The app required to open that link can’t be found + ’Link’ is link on a website, it couldn't be opened due to the required app not being found + + + Open Image in New Fire Tab + Open Image in New Fire Tab + Context menu item + + + Open Image in New Tab + Open Image in New Tab + Context menu item + + + Open in %@ + Open in %@ + Opening an entity in other application + + + Open in New Tab + Open in New Tab + Menu item that opens the link in a new tab + + + Open in New Window + Open in New Window + Menu item that opens the link in a new window + + + Open Link in New Fire Tab + Open Link in New Fire Tab + Context menu item + + + Open Link in New Tab + Open Link in New Tab + Context menu item + + + Open System Preferences + Open System Preferences + Open System Preferences (to re-enable permission for the App) (up to and including macOS 12 + + + Open System Settings… + Open System Settings… + This string represents a prompt or button label prompting the user to open system settings + + + Fireproof This Site + Fireproof This Site + Context menu item + + + Move Tab to New Window + Move Tab to New Window + Context menu item + + + Remove Fireproofing + Remove Fireproofing + Context menu item + + + This webpage has crashed. + This webpage has crashed. + Error page heading text shown when a Web Page process had crashed + + + Try reloading the page or come back later. + Try reloading the page or come back later. + Error page message text shown when a Web Page process had crashed + + + DuckDuckGo can’t load this page. + DuckDuckGo can’t load this page. + Error page heading text + + + Autofill + Autofill + Used as title for password management user interface + + + All Items + All Items + Used as title for the Autofill All Items option + + + Credit Cards + Credit Cards + Used as title for the Autofill Credit Cards option + + + Identities + Identities + Used as title for the Autofill Identities option + + + Lock + Lock + Lock Logins Vault menu + + + Passwords + Passwords + Used as title for the Autofill Logins option + + + Notes + Notes + Used as title for the Autofill Notes option + + + Save Address? + Save Address? + Title of dialog that allows the user to save an address method + + + Connected to %@ + Connected to %@ + In the password manager dialog, label that specifies the password manager vault we are connected with + + + Keeps you signed in after using the Fire Button + Keeps you signed in after using the Fire Button + In the password manager dialog, description of the section that allows the user to fireproof a website via a checkbox + + + Fireproof? + Fireproof? + In the password manager dialog, title of the section that allows the user to fireproof a website via a checkbox + + + Save Login to Bitwarden? + Save Login to Bitwarden? + Title of the passwored manager section of dialog that allows the user to save credentials + + + Unlock Bitwarden to Save + Unlock Bitwarden to Save + In the password manager dialog, alerts the user that they need to unlock Bitworden before being able to save the credential + + + Save Payment Method? + Save Payment Method? + Title of dialog that allows the user to save a payment method + + + Unlock + Unlock + Unlock Logins Vault menu + + + Delete + Delete + Button of the alert that asks the user to confirm they want to delete an password, login or credential to actually delete + + + Duplicate Password + Duplicate Password + Title of the alert that the password inserted already exists + + + You already have a password saved for this username and website. + You already have a password saved for this username and website. + Text of the alert that explains the password inserted already exists for a given website + + + Are you sure you want to delete this saved credit card? + Are you sure you want to delete this saved credit card? + Text of the alert that asks the user to confirm they want to delete a credit card + + + Are you sure you want to delete this saved autofill info? + Are you sure you want to delete this saved autofill info? + Text of the alert that asks the user to confirm they want to delete an identity + + + Are you sure you want to delete this note? + Are you sure you want to delete this note? + Text of the alert that asks the user to confirm they want to delete a note + + + Are you sure you want to delete this saved password + Are you sure you want to delete this saved password + Text of the alert that asks the user to confirm they want to delete a password + + + Save the changes you made? + Save the changes you made? + Text of the alert that asks the user if the want to save the changes made + + + If your logins are saved in another browser, you can import them into DuckDuckGo. + If your logins are saved in another browser, you can import them into DuckDuckGo. + In the password manager message when there are no items + + + No logins or credit card info yet + No logins or credit card info yet + In the password manager title when there are no items + + + Unlock your Autofill info + Unlock your Autofill info + In the password manager text of button to unlock autofill info + + + Password Manager + Password Manager + Section header + + + Paste from Clipboard + Paste from Clipboard + Paste button + + + Paste & Go + Paste & Go + Paste & Go button + + + Paste & Search + Paste & Search + Paste & Search button + + + Allow “%1$@“ to open %2$@ + Allow “%1$@“ to open %2$@ + Allow to open External Link (%@ 2) to open on current domain (%@ 1) + + + Allow the %1$@ to open “%2$@” links + Allow the %1$@ to open “%2$@” links + Allow the App Name(%@ 1) to open “URL Scheme”(%@ 2) links + + + Open this link in %@? + Open this link in %@? + Popover asking to open link in External App (%@) + + + “%1$@” would like to open this link in %2$@ + “%1$@” would like to open this link in %2$@ + Popover asking for domain %@ to open link in External App (%@) + + + Allow “%1$@“ to use your %2$@? + Allow “%1$@“ to use your %2$@? + Popover asking for domain %@ to use camera/mic/location (%@) + + + Allow “%@“ to open PopUp Window? + Allow “%@“ to open PopUp Window? + Popover asking for domain %@ to open Popup Window + + + Allow “%@“ to open PopUp Windows? + Allow “%@“ to open PopUp Windows? + Popover asking for domain %@ to open Popup Window + + + Camera + Camera + Camera input media device name + + + Camera and Microphone + Camera and Microphone + camera and microphone input media devices name + + + %1$@ access is disabled for %2$@ + %1$@ access is disabled for %2$@ + The app (DuckDuckGo: %@ 2) has no access permission to (%@ 1) media device + + + System location services are disabled + System location services are disabled + Geolocation Services are disabled in System Preferences + + + Open %@ + Open %@ + Open %@ App Name + + + Location + Location + User's Geolocation permission access name + + + Microphone + Microphone + Microphone input media device name + + + Pause %1$@ use on “%2$@” + Pause %1$@ use on “%2$@” + Temporarily pause input media device %@ access for %@2 website + + + Open System Settings + Open System Settings + Open System Settings (to re-enable permission for the App) (macOS 13 and above) + + + Deny + Deny + Permission Popover: Deny Website input media device access + + + Allow + Allow + Button that the user can use to authorise a web site to for, for example access location or camera and microphone etc. + + + Pop-up Blocked + Pop-up Blocked + Text of popver warning the user that the a pop-up as been blocked + + + Learn more about location services + Learn more about location services + Text of link that leads to web page with more informations about location services. + + + %@ + %@ + Open %@ URL Pop-up + + + Blocked Pop-ups + Blocked Pop-ups + Title of a popup that has a list of blocked popups + + + Pop-ups + Pop-ups + Open Pop Up Windows permission access name + + + Reload to ask permission again + Reload to ask permission again + Reload webpage to ask for input media device access permission again + + + Resume %1$@ use on “%2$@” + Resume %1$@ use on “%2$@” + Resume input media device %@ access for %@ website + + + Pin Tab + Pin Tab + Menu item. Pin as a verb + + + Hide Autofill Shortcut + Hide Autofill Shortcut + Menu item for hiding the autofill shortcut + + + Hide Bookmarks Shortcut + Hide Bookmarks Shortcut + Menu item for hiding the bookmarks shortcut + + + Hide Downloads Shortcut + Hide Downloads Shortcut + Menu item for hiding the downloads shortcut + + + Hide VPN Shortcut + Hide VPN Shortcut + Menu item for hiding the NetP shortcut + + + Show Autofill Shortcut + Show Autofill Shortcut + Menu item for showing the autofill shortcut + + + Show Bookmarks Shortcut + Show Bookmarks Shortcut + Menu item for showing the bookmarks shortcut + + + Show Downloads Shortcut + Show Downloads Shortcut + Menu item for showing the downloads shortcut + + + Show VPN Shortcut + Show VPN Shortcut + Menu item for showing the NetP shortcut + + + Reactivate + Reactivate + Activate button + + + Reactivate Duck Address + Reactivate Duck Address + Activate private email address button + + + Add Credit Card + Add Credit Card + Add New Credit Card button + + + Add Identity + Add Identity + Add New Identity button + + + Add Password + Add Password + Add New Login button + + + Add New + Add New + Add New item button + + + Added + Added + Label for login added data + + + Address 1 + Address 1 + Label for address 1 title + + + Address 2 + Address 2 + Label for address 2 title + + + City + City + Label for city title + + + Postal Code + Postal Code + Label for postal code title + + + State/Province + State/Province + Label for state/province title + + + Cancel + Cancel + Cancel button + + + Cardholder Name + Cardholder Name + Label for cardholder name title + + + CVV + CVV + Label for CVV title + + + Expiration Date + Expiration Date + Label for expiration date title + + + Card Number + Card Number + Label for card number title + + + Day + Day + Label for Day title + + + Deactivate + Deactivate + Deactivate button + + + Deactivate Duck Address + Deactivate Duck Address + Deactivate private email address button + + + Delete + Delete + Delete button + + + Edit + Edit + Edit button + + + Email Address + Email Address + Label for email address title + + + No Cards + No Cards + Label for cards empty state title + + + If your passwords are saved in another browser, you can import them into DuckDuckGo. + If your passwords are saved in another browser, you can import them into DuckDuckGo. + Label for default empty state description + + + No passwords or credit cards saved yet + No passwords or credit cards saved yet + Label for default empty state title + + + No Identities + No Identities + Label for identities empty state title + + + No passwords + No passwords + Label for logins empty state title + + + No Notes + No Notes + Label for notes empty state title + + + Enable Email Protection + Enable Email Protection + Text link to email protection website + + + Identification + Identification + Label for identification title + + + Address + Address + Default title for Addresses/Identities + + + Last Updated + Last Updated + Label for last updated edit field + + + Your autofill info will remain unlocked until your computer is idle for %@. + Your autofill info will remain unlocked until your computer is idle for %@. + Message about the duration for which autofill information remains unlocked on the lock screen. + + + Change in + Change in + Label used for a button that opens preferences + + + Settings + Settings + Label used for a button that opens preferences + + + unlock access to your autofill info + unlock access to your autofill info + Label presented when autofilling credit card information + + + change your autofill info access settings + change your autofill info access settings + Label presented when changing Auto-Lock settings + + + export your usernames and passwords + export your usernames and passwords + Label presented when exporting logins + + + unlock access to your autofill info + unlock access to your autofill info + Label presented when unlocking Autofill + + + 1 hour + 1 hour + Label used when selecting the Auto-Lock threshold + + + 1 minute + 1 minute + Label used when selecting the Auto-Lock threshold + + + 5 minutes + 5 minutes + Label used when selecting the Auto-Lock threshold + + + 12 hours + 12 hours + Label used when selecting the Auto-Lock threshold + + + 15 minutes + 15 minutes + Label used when selecting the Auto-Lock threshold + + + 30 minutes + 30 minutes + Label used when selecting the Auto-Lock threshold + + + Month + Month + Label for Month title + + + First Name + First Name + Label for first name title + + + Last Name + Last Name + Label for last name title + + + Middle Name + Middle Name + Label for middle name title + + + Credit Card + Credit Card + Label for new card title + + + Identity + Identity + Label for new identity title + + + Password + Password + Label for new login title + + + Note + Note + Label for new note title + + + Note + Note + Label for note title + + + Empty note + Empty note + Label for empty note title + + + Notes + Notes + Label for notes edit field + + + Password + Password + Label for password edit field + + + Phone Number + Phone Number + Label for phone number title + + + Emails sent to %@ will again be forwarded to your inbox. + Emails sent to %@ will again be forwarded to your inbox. + Text for the confirmation message displayed when a user tries activate a Private Email Address + + + Reactivate Private Duck Address? + Reactivate Private Duck Address? + Title for the confirmation message displayed when a user tries activate a Private Email Address + + + Duck Address Active + Duck Address Active + Mesasage displayed when a private email address is active + + + Emails sent to %@ will no longer be forwarded to your inbox. + Emails sent to %@ will no longer be forwarded to your inbox. + Text for the confirmation message displayed when a user tries deactivate a Private Email Address + + + Deactivate Private Duck Address? + Deactivate Private Duck Address? + Title for the confirmation message displayed when a user tries deactivate a Private Email Address + + + Management of this address is temporarily unavailable. + Management of this address is temporarily unavailable. + Mesasage displayed when a user tries to manage a private email address but the service is not available, returns an error or network is down + + + Duck Address Deactivated + Duck Address Deactivated + Mesasage displayed when a private email address is inactive + + + Got it + Got it + Button text for the alert dialog telling the user an updated username is no longer a private email address + + + You can still manage this Duck Address from emails received from it in your personal inbox. + You can still manage this Duck Address from emails received from it in your personal inbox. + Content for the alert dialog telling the user an updated username is no longer a private email address + + + Private Duck Address username was removed + Private Duck Address username was removed + Title for the alert dialog telling the user an updated username is no longer a private email address + + + Save + Save + Save button + + + Save password? + Save password? + Title for the editable Save Credentials popover + + + New Password Saved + New Password Saved + Title for the non-editable Save Credentials popover + + + %@ to manage your Duck Addresses on this device. + %@ to manage your Duck Addresses on this device. + Message displayed to the user when they are logged out of Email protection. + + + Newest First + Newest First + Label for Ascending date sort order + + + Oldest First + Oldest First + Label for Descending date sort order + + + Date Created + Date Created + Label for Date Created sort parameter + + + Date Modified + Date Modified + Label for Date Modified sort parameter + + + Title + Title + Label for Title sort parameter + + + Alphabetically + Alphabetically + Label for Ascending string sort order + + + Reverse Alphabetically + Reverse Alphabetically + Label for Descending string sort order + + + Username + Username + Label for username edit field + + + Website URL + Website URL + Label for website edit field + + + Year + Year + Label for Year title + + + Address: + Address: + Homepage address field label + + + Specific page + Specific page + Option to control Specific Home Page + + + New Tab page + New Tab page + Option to open a new tab + + + Set Homepage + Set Homepage + Set Homepage dialog title + + + Set Page… + Set Page… + Option to control the Specific Page + + + When navigating home or opening new windows. + When navigating home or opening new windows. + Homepage behavior description + + + Homepage + Homepage + Title for Homepage section in settings + + + About + About + Show about screen + + + About DuckDuckGo + About DuckDuckGo + About screen + + + More at %@ + More at %@ + Link to the about page + + + Privacy Policy + Privacy Policy + Link to privacy policy page + + + Privacy, simplified. + Privacy, simplified. + About screen + + + Send Feedback + Send Feedback + Feedback button in the about preferences page + + + DuckDuckGo is no longer providing browser updates for your version of macOS. + DuckDuckGo is no longer providing browser updates for your version of macOS. + This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS + + + Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates. + Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates. + Copy in section that tells the user to update their macOS version since their current version is unsupported + + + Appearance + Appearance + Show appearance preferences + + + Address Bar + Address Bar + Theme preferences + + + Autocomplete suggestions + Autocomplete suggestions + Option to show autocomplete suggestions in the address bar + + + Full website address + Full website address + Option to show full URL in the address bar + + + Theme + Theme + Theme preferences + + + Dark + Dark + In the preferences for themes, the option to select for activating dark mode in the app. + + + Light + Light + In the preferences for themes, the option to select for activating light mode in the app. + + + System + System + In the preferences for themes, the option to select for use the change the mode based on the system preferences. + + + Zoom + Zoom + Zoom settings section title + + + Default page zoom + Default page zoom + Default page zoom picker title + + + Autofill + Autofill + Show Autofill preferences + + + Default Browser + Default Browser + Show default browser preferences + + + DuckDuckGo is your default browser + DuckDuckGo is your default browser + Indicate that the browser is the default + + + Make DuckDuckGo Default… + Make DuckDuckGo Default… + represents a prompt message asking the user to make DuckDuckGo their default browser. + + + DuckDuckGo is not your default browser. + DuckDuckGo is not your default browser. + Indicate that the browser is not the default + + + Downloads + Downloads + Show downloads browser preferences + + + Duck Player + Duck Player + Show Duck Player browser preferences + + + General + General + Show general preferences + + + On Startup + On Startup + Name of the preferences section related to app startup + + + Privacy + Privacy + Show privacy browser preferences + + + Reopen all windows from last session + Reopen all windows from last session + Option to control session restoration + + + Open a new window + Open a new window + Option to control session startup + + + Sync & Backup + Sync & Backup + Show sync preferences + + + Unlock device to setup Sync & Backup + Unlock device to setup Sync & Backup + Reason for auth when setting up Sync + + + VPN + VPN + Show VPN preferences + + + Print… + Print… + Menu item title + + + Quit + Quit + Quit button + + + Reload Page + Reload Page + Context menu item + + + Remove Favorite + Remove Favorite + Remove Favorite button + + + Remove from Favorites + Remove from Favorites + Button for removing bookmarks from favorites + + + Reopen Last Closed Tab + Reopen Last Closed Tab + This string represents an action to reopen the last closed tab in the browser + + + Reopen Last Closed Window + Reopen Last Closed Window + This string represents an action to reopen the last closed window in the browser + + + Report Broken Site + Report Broken Site + Menu with feedback commands + + + Restart Bitwarden + Restart Bitwarden + Button to restart Bitwarden application + + + Bitwarden is not responding. Please restart it to initiate the communication again + Bitwarden is not responding. Please restart it to initiate the communication again + This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again. + + + Save + Save + Save button + + + Save Image As… + Save Image As… + Context menu item + + + Scroll to find the App Settings (All Accounts) section. + Scroll to find the App Settings (All Accounts) section. + Setup of the integration with Bitwarden app + + + Search with DuckDuckGo + Search with DuckDuckGo + Context menu item + + + Select Bitwarden → Preferences from the Mac menu bar. + Select Bitwarden → Preferences from the Mac menu bar. + Setup of the integration with Bitwarden app (up to and including macOS 12) + + + Select Bitwarden → Settings from the Mac menu bar. + Select Bitwarden → Settings from the Mac menu bar. + Setup of the integration with Bitwarden app (macOS 13 and above) + + + Send Browser Feedback + Send Browser Feedback + Menu with feedback commands + + + Your feedback will help us improve the DuckDuckGo app. + Your feedback will help us improve the DuckDuckGo app. + Text shown to the user when they provide feedback. + + + General feedback + General feedback + Name of the option the user can chose to give general browser feedback + + + Report a problem + Report a problem + Name of the option the user can chose to give browser feedback about a problem they enountered + + + Request a feature + Request a feature + Name of the option the user can chose to give browser feedback about a feature they would like + + + Select a category + Select a category + Title of the picker where the user can chose the category of the feedback they want ot send. + + + Thank you! + Thank you! + Thanks the user for sending feedback + + + Help Improve the DuckDuckGo Browser + Help Improve the DuckDuckGo Browser + Title of the interface to send feedback on the browser + + + Settings + Settings + Menu item for opening settings + + + Share + Share + Menu item title + + + Create QR Code + Create QR Code + Menu item title + + + More… + More… + Sharing Menu -> More… + + + Show Folder Contents + Show Folder Contents + Menu item that shows the content of a folder + + + Submit + Submit + Submit button + + + Submit Report + Submit Report + Submit Report button + + + Bookmarks + Bookmarks + Tab bookmarks title + + + Failed to open page + Failed to open page + Tab error title + + + New Tab + New Tab + Tab home title + + + Welcome + Welcome + Tab onboarding title + + + Settings + Settings + Tab preferences title + + + Add to Favorites + Add to Favorites + Tooltip for add to favorites button + + + Open application menu + Open application menu + Tooltip for the Application Menu button + + + Add item + Add item + Tooltip for the Add Item button + + + More options + More options + Tooltip for the More Options button + + + Autofill + Autofill + Tooltip for the autofill shortcut + + + Bookmark this page + Bookmark this page + Tooltip for the Add Bookmark button + + + Edit bookmark + Edit bookmark + Tooltip for the Edit Bookmark button + + + Manage bookmarks + Manage bookmarks + Tooltip for the Manage Bookmarks button + + + New bookmark + New bookmark + Tooltip for the New Bookmark button + + + New folder + New folder + Tooltip for the New Folder button + + + Bookmarks + Bookmarks + Tooltip for the bookmarks shortcut + + + Clear browsing history for %@ + Clear browsing history for %@ + Tooltip for burn button where %@ is the domain + + + Clear browsing history and data for %@ + Clear browsing history and data for %@ + Tooltip for burn button where %@ is the domain + + + Clear download history + Clear download history + Tooltip for the Clear Downloads button + + + Open downloads folder + Open downloads folder + Tooltip for the Open Downloads Folder button + + + Downloads + Downloads + Tooltip for the downloads shortcut + + + Close find bar + Close find bar + Tooltip for the Find In Page bar's Close button + + + Next result + Next result + Tooltip for the Find In Page bar's Next button + + + Previous result + Previous result + Tooltip for the Find In Page bar's Previous button + + + Clear browsing history + Clear browsing history + Tooltip for the Fire button + + + Home + Home + Tooltip for the home button + + + Show the previous page +Hold to show history + Show the previous page +Hold to show history + Tooltip for the Back button + + + Show the next page +Hold to show history + Show the next page +Hold to show history + Tooltip for the Forward button + + + Reload this page + Reload this page + Tooltip for the Refresh button + + + Stop loading this page + Stop loading this page + Tooltip for the Stop Navigation button + + + Show the Privacy Dashboard and manage site settings + Show the Privacy Dashboard and manage site settings + Tooltip for the Privacy Dashboard button + + + Open a new tab + Open a new tab + Tooltip for the New Tab button + + + Uninstall + Uninstall + Uninstall button + + + Unmute Tab + Unmute Tab + Menu item. Unmute tab + + + Unpin Tab + Unpin Tab + Menu item. Unpin as a verb + + + Your version of macOS is no longer supported. + Your version of macOS is no longer supported. + his string represents the header for an alert informing the user that their version of macOS is no longer supported + + + Update + Update + Update button + + + Version %1$@ (%2$@) + Version %1$@ (%2$@) + Displays the version and build numbers + + + DuckDuckGo automatically blocks hidden trackers as you browse the web. + DuckDuckGo automatically blocks hidden trackers as you browse the web. + feature explanation in settings + + + Web Tracking Protection + Web Tracking Protection + Web tracking protection settings section title + + + Zoom + Zoom + Menu with Zooming commands + + + •••••••••••• + •••••••••••• + + + + ⚠️ Notes are deprecated. + ⚠️ Notes are deprecated. + + + +
+ +
+ +
+ + + (%@) + (%@) + + + + Sorry, this code is invalid. Please make sure it was entered correctly. + Sorry, this code is invalid. Please make sure it was entered correctly. + Description for invalid code error + + + Sync & Backup Error + Sync & Backup Error + Title for sync error alert + + + Unable to create the recovery PDF. + Unable to create the recovery PDF. + Description for unable to create recovery pdf error + + + Unable to delete data on the server. + Unable to delete data on the server. + Description for unable to delete data error + + + To pair these devices, turn off Sync & Backup on one device then tap "Sync With Another Device" on the other device. + To pair these devices, turn off Sync & Backup on one device then tap "Sync With Another Device" on the other device. + Description for unable to merge two accounts error + + + Unable to remove this device from Sync & Backup. + Unable to remove this device from Sync & Backup. + Description for unable to remove device error + + + Unable to connect to the server. + Unable to connect to the server. + Description for unable to sync to server error + + + Unable to Sync with another device. + Unable to Sync with another device. + Description for unable to sync with another device error + + + Unable to turn Sync & Backup off. + Unable to turn Sync & Backup off. + Description for unable to turn sync off error + + + Unable to update the device name. + Unable to update the device name. + Description for unable to update device name error + + + Cancel + Cancel + Cancel button + + + Copy + Copy + Copy button + + + Done + Done + Done button + + + Next + Next + Next button + + + Not Now + Not Now + Not Now button + + + OK + OK + OK button + + + Paste + Paste + Paste button + + + Paste from Clipboard + Paste from Clipboard + Paste from Clipboard button + + + Sync With Another Device + Sync With Another Device + Button text on the Begin Syncing card in sync settings + + + Securely sync bookmarks and passwords between your devices. + Securely sync bookmarks and passwords between your devices. + Begin Syncing card description in sync settings + + + Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key. + Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key. + Footer / caption on the Begin Syncing card in sync settings + + + Begin Syncing + Begin Syncing + Begin Syncing card title in sync settings + + + Paste Code Here + Paste Code Here + Sync enter recovery code dialog first possible action + + + or scan QR code with a device that is still connected + or scan QR code with a device that is still connected + Sync enter recovery code dialog second possible action + + + Enter the code on your Recovery PDF, or another synced device, to recover your synced data. + Enter the code on your Recovery PDF, or another synced device, to recover your synced data. + Sync enter recovery code dialog subtitle + + + Enter Code + Enter Code + Sync enter recovery code dialog title + + + Other Options + Other Options + Sync settings. Other Options section title + + + Connecting… + Connecting… + Sync preparing to sync dialog action + + + We're setting up the connection to synchronize your bookmarks and saved logins with the other device. + We're setting up the connection to synchronize your bookmarks and saved logins with the other device. + Preparing to sync dialog subtitle during sync set up + + + Preparing To Sync + Preparing To Sync + Preparing to sync dialog title during sync set up + + + Recover Synced Data + Recover Synced Data + Sync settings. Link to recover synced data. + + + Get Started + Get Started + Sync recover synced data dialog button + + + To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync. + To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync. + Recover synced data during Sync recovery process dialog subtitle + + + Recover Synced Data + Recover Synced Data + Sync recover synced data dialog title + + + Sync & Backup + Sync & Backup + Show sync preferences + + + Sync and Back Up This Device + Sync and Back Up This Device + Sync settings. Title of a link to start setting up sync and backup the device + + + Sync Enabled + Sync Enabled + Sync state is enabled + + + Details... + Details... + Sync Settings device details button + + + Remove... + Remove... + Button to remove a device + + + Remove Device + Remove Device + Button text on remove a device confirmation button + + + "%@" will no longer be able to access your synced data. + "%@" will no longer be able to access your synced data. + Message to confirm the device will no longer be able to access the synced data - devoce name item inserted + + + Remove device? + Remove device? + Title on remove a device confirmation + + + Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices. + Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices. + Description of rollout banner + + + Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device. + Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device. + Sync with another device dialog subtitle - Instruction with sync menu path item inserted + + + Sync With Another Device + Sync With Another Device + Sync with another device dialog title + + + Enter Code + Enter Code + Text on enter code button on Sync with another device dialog + + + Paste the code here to sync. + Paste the code here to sync. + Sync with another device dialog enter code explanation + + + Show Code + Show Code + Text on show code button on Sync with another device dialog + + + Share this code to connect with a desktop machine. + Share this code to connect with a desktop machine. + Sync with another device dialog show code explanation + + + Scan this QR code to connect. + Scan this QR code to connect. + Sync with another device dialog show qr code explanation + + + View QR Code + View QR Code + Sync with another device dialog view qr code link + + + View Text Code + View Text Code + Sync with another device dialog view text code link + + + Turn On Sync & Backup + Turn On Sync & Backup + Sync with server dialog button + + + This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices. + This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices. + Sync with server dialog first subtitle + + + The encryption key is only stored on your device, DuckDuckGo cannot access it. + The encryption key is only stored on your device, DuckDuckGo cannot access it. + Sync with server dialog second subtitle + + + Sync and Back Up This Device + Sync and Back Up This Device + Sync with server dialog title + + + Synced Devices + Synced Devices + Settings section title + + + This Device + This Device + Indicator of a current user's device on the list + + + Turn Off + Turn Off + Turn off sync confirmation dialog button title + + + Turn Off and Delete Server Data… + Turn Off and Delete Server Data… + Disable and delete data sync button caption + + + This device will no longer be able to access your synced data. + This device will no longer be able to access your synced data. + Turn off sync confirmation dialog message + + + Turn off sync? + Turn off sync? + Turn off sync confirmation dialog title + + + Turn Off Sync… + Turn Off Sync… + Disable sync button caption + + + Manage Bookmarks + Manage Bookmarks + Button title for sync bookmarks limits exceeded warning to go to manage bookmarks + + + Bookmark limit exceeded. Delete some to resume syncing. + Bookmark limit exceeded. Delete some to resume syncing. + Description for sync bookmarks limits exceeded warning + + + Manage passwords… + Manage passwords… + Button title for sync credentials limits exceeded warning to go to manage passwords + + + Logins limit exceeded. Delete some to resume syncing. + Logins limit exceeded. Delete some to resume syncing. + Description for sync credentials limits exceeded warning + + + Delete Data + Delete Data + Label for delete account button + + + These devices will be disconnected and your synced data will be deleted from the server. + These devices will be disconnected and your synced data will be deleted from the server. + Message for delete account confirmation pop up + + + Delete server data? + Delete server data? + Title for delete account confirmation pop up + + + Name + Name + The text entry label to name the device + + + Device name + Device name + The text entry prompt to name the device + + + Device Details + Device Details + The title of the device details dialog + + + Your data is synced! + Your data is synced! + Sync setup confirmation dialog title + + + Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced. + Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced. + Text for fetch favicons onboarding dialog + + + Download Missing Icons? + Download Missing Icons? + Title for fetch favicons onboarding dialog + + + Automatically download icons for synced bookmarks. Icon downloads are exposed to your network. + Automatically download icons for synced bookmarks. Icon downloads are exposed to your network. + Caption for fetch favicons option + + + Auto-Download Icons + Auto-Download Icons + Title for fetch favicons option + + + Keep Bookmarks Icons Updated + Keep Bookmarks Icons Updated + Title of the confirmation button for favicons fetching + + + Sync Paused + Sync Paused + Title for sync limits exceeded warning + + + Options + Options + Title for options settings + + + Recovery + Recovery + Sync settings section title + + + If you lose your device, you will need this recovery code to restore your synced data. + If you lose your device, you will need this recovery code to restore your synced data. + Instructions on how to restore synced data + + + Copy Code + Copy Code + Sync recovery PDF copy code button + + + If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF. + If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF. + Sync recovery PDF explanation + + + Save PDF + Save PDF + Sync recovery PDF save pdf button + + + Anyone with access to this code can access your synced data, so please keep it in a safe place. + Anyone with access to this code can access your synced data, so please keep it in a safe place. + Sync recovery PDF warning + + + Save Your Recovery Code + Save Your Recovery Code + Caption for a button to save Sync recovery PDF + + + Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate. + Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate. + Caption for share favorite option + + + Unify Favorites Across Devices + Unify Favorites Across Devices + Title for share favorite option + + + Share + Share + Share button + + + Submit + Submit + Submit button + + + Settings › Sync & Backup + Settings › Sync & Backup + Sync Menu Path + + + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Data syncing unavailable warning message + + + Sync & Backup is Paused + Sync & Backup is Paused + Title of the warning message that Sync & Backup is Paused + + + Sync & Backup is Unavailable + Sync & Backup is Unavailable + Title of the warning message that sync and backup are unavailable + + + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Data syncing unavailable warning message + + +
+
From f71541b7634ee1138fd98a528964aeff7c7ab56d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 12 Apr 2024 21:43:27 +0500 Subject: [PATCH 081/221] Fix WebView hanging input (#2616) Task/Issue URL: https://app.asana.com/0/1177771139624306/1206990108527681/f Description: fixes the mouse event queue overflow issue when loading websites (see https://app.asana.com/0/0/1207061219313301/f) the fix is done by proxying NSTrackingArea delegate and suppressing mouse events while Web View is loading --- DuckDuckGo/Tab/View/WebView.swift | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/DuckDuckGo/Tab/View/WebView.swift b/DuckDuckGo/Tab/View/WebView.swift index bc578f6d19..b2c458cee0 100644 --- a/DuckDuckGo/Tab/View/WebView.swift +++ b/DuckDuckGo/Tab/View/WebView.swift @@ -35,6 +35,30 @@ final class WebView: WKWebView { weak var contextMenuDelegate: WebViewContextMenuDelegate? weak var interactionEventsDelegate: WebViewInteractionEventsDelegate? + private var isLoadingObserver: Any? + + override func addTrackingArea(_ trackingArea: NSTrackingArea) { + /// disable mouseEntered/mouseMoved/mouseExited events passing to Web View while it‘s loading + /// see https://app.asana.com/0/1177771139624306/1206990108527681/f + if trackingArea.owner?.className == "WKMouseTrackingObserver" { + // suppress Tracking Area events while loading + isLoadingObserver = self.observe(\.isLoading, options: [.new]) { [weak self, trackingArea] _, c in + if c.newValue /* isLoading */ ?? false { + guard let self, self.trackingAreas.contains(trackingArea) else { return } + removeTrackingArea(trackingArea) + } else { + guard let self, !self.trackingAreas.contains(trackingArea) else { return } + superAddTrackingArea(trackingArea) + } + } + } + super.addTrackingArea(trackingArea) + } + + private func superAddTrackingArea(_ trackingArea: NSTrackingArea) { + super.addTrackingArea(trackingArea) + } + override var isInFullScreenMode: Bool { if #available(macOS 13.0, *) { return self.fullscreenState != .notInFullscreen From 899bcb5cd412d5637c165c720a9e1e0d1216533d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 12 Apr 2024 09:47:19 -0700 Subject: [PATCH 082/221] [Release PR] Remove waitlist button state (#2620) Task/Issue URL: https://app.asana.com/0/1199230911884351/1207061569597820/f Tech Design URL: CC: Description: This PR removes the waitlist available NetP icon to make sure it never shows to users again. We don't need this asset at all any more so let's just remove it and no longer show the custom state. We'll come back later and properly clean up the waitlist UI code, this PR just does the bare minimum to fix the issue. --- .../Contents.json | 22 ------------------ .../Toolbar Button 1.pdf | Bin 5013 -> 0 bytes .../Toolbar Button.pdf | Bin 5013 -> 0 bytes .../NetworkProtectionNavBarButtonModel.swift | 8 ------- 4 files changed, 30 deletions(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Contents.json delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Toolbar Button 1.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Toolbar Button.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Contents.json deleted file mode 100644 index 89e4996541..0000000000 --- a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "Toolbar Button.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Toolbar Button 1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Toolbar Button 1.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Toolbar Button 1.pdf deleted file mode 100644 index 6fca4a04e30049c80a2a9521f3e9e308873702cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5013 zcma)=+m0JW5QgvPDf%W-Kw`Gf2PujKNk9k?VZ$Bb!Z=P?xAQZ8ye_*~(*{ zK4s^{bM+#8^iKCNJD)xkOS`+*LT&I5IDWgkdwcrT1i)+Abv)hgHn+po^W9%J+uik7 zUzlgF)_-k}!ym)d3uoRQ2RBY`SRCq$&vd1~)$?z<*xoLZx=k0y?cJbTT|P_S4q+@Q z)e_AD+R@jLQ#Q_zxh0#MP;oi=Z0eX?4Yir=Af!@p1uPaHVp<4;_!uI2I~%HFC`pnt z%ZX#ig;+wI9d)INrr@pPr%AW3vd6a&+F8z(BA9F0`m)w;TlM~dy&k)jJzRCs#$W!1n)e%O7t04>Q))+?3N;1!t7i~gikRL@u7lv zUlMTvk>E*SlP%Fj?2(@2C}FVXYQY;=#n5~*ZuHrQmO%pPXcZ*c5JE*Rk1cro0V@Ty z-sCZfSTi2E=)47SmArXew-GA|xrBBbVl@#~HQHhliFYXk78RgYL6U2wi0hI*$wTEw zn{1lG(Rj>$XrnDHQvwpA32R8^kVUeVT3s`ZbVtEl zK}0te;gHCe=<100Zsr}RY42JxhZg=EzzOiTZ%PEcrwxS ziP$;f6H_ASWsKRSLSw{pIF_DLNFtO%#nsh5T<|4QI*Yb8k`hOAt*tsck^x7!K)v^k zID-)fCq3%M;H)RbV5!s`z$mIhh%=m7=a8&R#Gtth2@xUVBpRsLnuG#cTBt-13O-21 zAnHbXqfq$*=2Wo(U}&VlKjwAkaXh znpj)gqU4Zp(Gv8O#6Qv2(&2?U$TLsuzZ$Vf=2REAsjK$$8la7trhTBHEP z5Ufn1Ni-8pGRz1ecM@xz=0Iq8Fw14`603>uYF`KU!&+EoHr;^TM$njweXtX9AOeNv zKL5*1rBGuuJ#n1Q3Aj+SG_AybWv#d40&Rz1Y0a2zk#Zz)p;BdX7+EGB)6mnTB~fOS zs?22>NhE;r2;vh39c9#D^~G}4ozA6HWF9Cf6|(th|vV2HQ{d7ry@1XmyA?9c&pD=bJqvraK#<^q*~D^_Na zaD-Zm2_h1}Lh;Jz?cCP&uH=?-@9fBeR{KJ4RqTQBSmcmJG?YLW<4SY7E=ibXOi$a$ zq?ZG2Ie#TE@{nEKrAkFt|5=sx#p(#x?Ydb`2$k!LJ=kP?br;eos8U5#sa!U?3gx
VMi}_tGz`VWsg~Ux0|BzW}5_n@y^&&WTj0p^-{X1OMVDb+0{O^K!{7jt%b!U*9TQE zSM%`Eb?-GEPD4xKYvEijDg4SL;sT_Yst)I$6wxqj2i}^xG6nZ5km1T@ zl;fZAGcR=irJ?NynC++W{l1A?MyLAFi8Jb$V%%|5@#SJ=L3zl{jq3xK35HZxsx6}Y z6;Z=7zTf91Y}KlY$Y}1|6jNEQ%QJ07bg^nx#a69a&a{=k)_`(QWaYVXc)(V7O8N9$ zDYj%;jV_RKYON^~<*Uewud)!nZ~Ez=TD8PK@aFoT?ZH5|yt8mqtJ58T z3ttAxM%qI)s$;8;$|G0&Di=pyF}Oa+i-J~SUnsJ2LNS>VU4>#gF;%-F%3(EY6bd>?>lq#a`SfAf$w&&Z#G{X-kG14(^u&Q^9jDEjVx_hzuB40GYhzlM7ZZ%4rWk&g=U z7z%rSpHt?d}uv_~!K| nb5O3X4~LTxho=Bvy}J2l2m9%x)#m;*xf3}rhewZIeE;RYY6aTk diff --git a/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Toolbar Button.pdf b/DuckDuckGo/Assets.xcassets/Images/NetworkProtectionWaitlist/NetworkProtectionAvailableButton.imageset/Toolbar Button.pdf deleted file mode 100644 index 04826a0a10d42f4ab66fd659667c8f5c10934e8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5013 zcma)=+m0JW5QgvPDf%W7ATitLgM=bM5)cAJvf&PKVH_tc@~&ZRP~hqLYGz#18wbc- zB)z}8tNuFn;nDM_Uw-PwG7QdGbNKn!!I;lJGgr^vp1yi{+MIUtUw-;;yD@gGRzCCi zF*7f|t7oB`8UJ&p`;eKBAB(2l-7BFs_y-)n-QB%8{bB;(wahx6?suEp;p*w`&ztS; z`l~O@lb8Kp+vD*2aP`cYH^;$^lNlC+mcgew(%2<^Pil_HpH+4|B;x2<~rK;PoKP9LVaXk#z| zGL{TLfOn_~ri|I9kmDq>r5xZ{#uBn~GnwF>XI6a7h-Ut+%p3)N4ljH6R^WN@0YpatXHNmX!hhBMsXtl8T(k*CC( z#~?7VXrheZ(A=_^E+8|>X_Tee%-BMlJCGwe(QK#CV22Cz8t*Gb=oNBJIISJ8Dcr-DEW& zO^f)D#9vqiggX;$j3zC7%Ty2$VI?Whn}`VoT_t@XBQ7CRX>QpAWwNZmDUFG4 zkpd7+u+oX9r5SG$VOj{W6JKjL2SUPwUM_tXUrmI&c?-B7`o+?-sRr~moJLn%8aplr z!cjiqqLfHOOiO#DLQOFfCbv_RPYghCi)y%=P8shHtQ&J5lS~? z;~^0|bt4{5BQ+`VF(n1hR!3+o<%dl;29H~ice#s2aP>j<4sAfU!h-PA>l71aE|3YB zVx<=eN2s-!AR-PdB(JpIgE8AwNP zQ_@}7AeO!UWVjn%Oh**|!ABHV&V5*$OR%AQ)(aE0IVM%^aOzqTe?`y!Rzwcxw7%I9I*=umPQ(F7F-s2I))KbkgWukl)>G&!O;rn{o9aO3|$@kn`|D`TE4aO8@?@j)&Mnu*Imk$PmOGaZ%gFL)`^@luTQ*;#~Q>Is#uh;r!mImOmMy%^-xVtJA{zhB5x#r(z+ zeen9M!7kc8y?lOmcQ_r*k3Y>1ELT7L_1n%|J>R_9Ex>oXS2vq44sXqm>+Y-cg4(c- z&XTTo$HO1@+uhN~W6$*>?#bcqw7Wy2ysn(V=bL*l!xlS1-5ZQR>pRgmH?N>-#DZ&c z*7M}>2W#C0|075ziJiZbBnBQ;Aumgd;ZHWF&HnJ_;nKJFyVrv?WtbCB{xzIqxE%p6 zpFAqaXGrY%liS@iQYdmmxPU4q%D2wqruHtx;ZC8`{pM!ByC3A??CYnv@%!%xm!=p#fzW?$c3r^bN diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index ee8b4d550b..de9f3ee692 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -116,14 +116,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { guard [.normal, .integrationTests].contains(NSApp.runType) else { return NSImage() } #endif - if NetworkProtectionWaitlist().readyToAcceptTermsAndConditions { - return .networkProtectionAvailableButton - } - - if NetworkProtectionKeychainTokenStore().isFeatureActivated { - return .image(for: icon)! - } - return .image(for: icon)! } From 6d625711381ac761346115f86ecb1a68c866e865 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Fri, 12 Apr 2024 17:02:35 +0000 Subject: [PATCH 083/221] Bump version to 1.83.0 (162) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 3e6bef97fd..1155c5e451 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 161 +CURRENT_PROJECT_VERSION = 162 From 0c84df23e46b7de88a20618ca03da33a0138981c Mon Sep 17 00:00:00 2001 From: Alessandro Boron Date: Mon, 15 Apr 2024 21:35:03 +1000 Subject: [PATCH 084/221] Update Sparkle to 2.6.0 (#2450) Task/Issue URL: https://app.asana.com/0/0/1206873880033587/f --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 55941a8752..ba6aeca66d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14560,7 +14560,7 @@ repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; requirement = { kind = exactVersion; - version = 2.5.2; + version = 2.6.0; }; }; B65CD8C92B316DF100A595BB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 48d2d9aea8..a567f97be3 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle.git", "state" : { - "revision" : "47d3d90aee3c52b6f61d04ceae426e607df62347", - "version" : "2.5.2" + "revision" : "0a4caaf7a81eea2cece651ef4b17331fa0634dff", + "version" : "2.6.0" } }, { From 1695f1866f2ab4b6ab44f827731445eb5e17c237 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Mon, 15 Apr 2024 14:27:49 +0200 Subject: [PATCH 085/221] Update the title capitalization style (#2624) Task/Issue URL: https://app.asana.com/0/1199230911884351/1206924693423599/f **Description**: "Your subscription is being activated" should be in sentence case. **Steps to test this PR**: Getting the "Your subscription is being activated" state during subscription purchase may be hard to replicate as it requires BE failure. To force trigger the state, please hardcode for the `statePublisher` in `PreferencesSubscriptionModel` to always return `PreferencesSubscriptionState.subscriptionPendingActivation`. --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../SubscriptionUI/Sources/SubscriptionUI/UserText.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 645253c3a4..3d344a38c9 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -75,7 +75,7 @@ enum UserText { static let haveSubscriptionButton = NSLocalizedString("subscription.preferences.i.have.a.subscription.button", value: "I Have a Subscription", comment: "Button enabling user to activate a subscription user bought earlier or on another device") // MARK: Preferences when subscription activation is pending - static let preferencesSubscriptionPendingHeader = NSLocalizedString("subscription.preferences.subscription.pending.header", value: "Your Subscription is Being Activated", comment: "Header for the subscription preferences pane when the subscription activation is pending") + static let preferencesSubscriptionPendingHeader = NSLocalizedString("subscription.preferences.subscription.pending.header", value: "Your subscription is being activated", comment: "Header for the subscription preferences pane when the subscription activation is pending") static let preferencesSubscriptionPendingCaption = NSLocalizedString("subscription.preferences.subscription.pending.caption", value: "This is taking longer than usual, please check back later.", comment: "Caption for the subscription preferences pane when the subscription activation is pending") // MARK: Preferences when subscription is expired From ef05acd339cae141b02a0101e8fc8e4dd382a840 Mon Sep 17 00:00:00 2001 From: Pete Smith Date: Mon, 15 Apr 2024 13:55:57 +0100 Subject: [PATCH 086/221] Support Autofill Domains with Port Number Suffixes (#2602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1206023722645078/f **Description**: This PR includes the following changes: * BSK 134.1.0 * Adds a `URLExtension` method which fixes an new issue where the Autofill dialog didn’t open with the right account highlighted, when the domain had a port suffix * Adds tests for above --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../Common/Extensions/URLExtension.swift | 8 +++++++ .../View/NavigationBarViewController.swift | 5 ++-- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Common/Extensions/URLExtensionTests.swift | 24 +++++++++++++++++++ 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ba6aeca66d..a97d747dd3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -14544,7 +14544,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 134.0.1; + version = 134.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a567f97be3..e7045a615e 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "b0749d25996c0fa18be07b7851f02ebb3b9fab50", - "version" : "134.0.1" + "revision" : "90e789b95403481e7c2f0e4aa661890d4252f0e6", + "version" : "134.1.0" } }, { diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 3d95ae07f1..79d5e3e8ed 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -278,6 +278,14 @@ extension URL { return string } + func hostAndPort() -> String? { + guard let host else { return nil } + + guard let port = port else { return host } + + return "\(host):\(port)" + } + #if !SANDBOX_TEST_TOOL func toString(forUserInput input: String, decodePunycode: Bool = true) -> String { let hasInputScheme = input.hasOrIsPrefix(of: self.separatedScheme ?? "") diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 7b11248e0f..6b1af92c29 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -710,10 +710,11 @@ final class NavigationBarViewController: NSViewController { } popovers.passwordManagementDomain = nil - guard let url = url, let domain = url.host else { + guard let url = url, let hostAndPort = url.hostAndPort() else { return } - popovers.passwordManagementDomain = domain + + popovers.passwordManagementDomain = hostAndPort } private func updateHomeButton() { diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 0eb8a47757..ebd34e3874 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", exact: "134.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.1.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index f7a52283d8..0749090bd4 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.1.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index f50597c4ec..74c24caa6b 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: "134.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Common/Extensions/URLExtensionTests.swift b/UnitTests/Common/Extensions/URLExtensionTests.swift index 00ed9d6f24..5869636572 100644 --- a/UnitTests/Common/Extensions/URLExtensionTests.swift +++ b/UnitTests/Common/Extensions/URLExtensionTests.swift @@ -136,4 +136,28 @@ final class URLExtensionTests: XCTestCase { } } + func testWhenGetHostAndPort_WithPort_ThenHostAndPortIsReturned() throws { + // Given + let expected = "duckduckgo.com:1234" + let sut = URL(string: "https://duckduckgo.com:1234") + + // When + let result = sut?.hostAndPort() + + // Then + XCTAssertEqual(expected, result) + } + + func testWhenGetHostAndPort_WithoutPort_ThenHostReturned() throws { + // Given + let expected = "duckduckgo.com" + let sut = URL(string: "https://duckduckgo.com") + + // When + let result = sut?.hostAndPort() + + // Then + XCTAssertEqual(expected, result) + } + } From f9630cfa7fa615f1645ff8d881d0fb6e059b0c71 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 15 Apr 2024 18:55:36 +0500 Subject: [PATCH 087/221] Reimplement audio mute for App Store (#2593) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207043866291157/f --- .../Common/Extensions/WKWebView+Private.h | 2 - .../Extensions/WKWebViewExtension.swift | 126 +++++++++--------- .../Model/PinnedTabsViewModel.swift | 2 +- .../PinnedTabs/View/PinnedTabView.swift | 2 +- DuckDuckGo/Tab/Model/Tab.swift | 9 +- .../TabBar/View/TabBarViewController.swift | 7 +- DuckDuckGo/TabBar/View/TabBarViewItem.swift | 20 ++- UnitTests/Permissions/WebViewMock.swift | 17 +-- ...bViewPrivateMethodsAvailabilityTests.swift | 5 + .../TabBar/View/MockTabViewItemDelegate.swift | 6 +- 10 files changed, 100 insertions(+), 96 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/WKWebView+Private.h b/DuckDuckGo/Common/Extensions/WKWebView+Private.h index b5e8c44d99..daa465149f 100644 --- a/DuckDuckGo/Common/Extensions/WKWebView+Private.h +++ b/DuckDuckGo/Common/Extensions/WKWebView+Private.h @@ -63,8 +63,6 @@ typedef NS_OPTIONS(NSUInteger, _WKFindOptions) { - (void)_stopMediaCapture API_AVAILABLE(macos(10.15.4), ios(13.4)); - (void)_stopAllMediaPlayback; -- (_WKMediaMutedState)_mediaMutedState API_AVAILABLE(macos(11.0), ios(14.0));; -- (void)_setPageMuted:(_WKMediaMutedState)mutedState API_AVAILABLE(macos(10.13), ios(11.0)); @end diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index fcd19de900..5f7a7b316c 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -32,7 +32,17 @@ extension WKWebView { enum AudioState { case muted case unmuted - case notSupported + + init(wkMediaMutedState: _WKMediaMutedState) { + self = wkMediaMutedState.contains(.audioMuted) ? .muted : .unmuted + } + + mutating func toggle() { + self = switch self { + case .muted: .unmuted + case .unmuted: .muted + } + } } enum CaptureState { @@ -114,96 +124,84 @@ extension WKWebView { return .active } -#if !APPSTORE - private func setMediaCaptureMuted(_ muted: Bool) { - guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { - assertionFailure("WKWebView does not respond to selector _stopMediaCapture") - return + @objc dynamic var mediaMutedState: _WKMediaMutedState { + get { + // swizzle the method to call `_mediaMutedState` without performSelector: usage + guard Self.swizzleMediaMutedStateOnce else { return [] } + return self.mediaMutedState // call the original } - let mutedState: _WKMediaMutedState = { - guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } - return self._mediaMutedState() - }() - var newState = mutedState - if muted { - newState.insert(.captureDevicesMuted) - } else { - newState.remove(.captureDevicesMuted) + set { + // swizzle the method to call `_setPageMuted:` without performSelector: usage (as there‘s a non-object argument to pass) + guard Self.swizzleSetPageMutedOnce else { return } + self.mediaMutedState = newValue // call the original } - guard newState != mutedState else { return } - self._setPageMuted(newState) } -#endif - func muteOrUnmute() { -#if !APPSTORE - guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { - assertionFailure("WKWebView does not respond to selector _stopMediaCapture") - return + static private let swizzleMediaMutedStateOnce: Bool = { + guard let originalMethod = class_getInstanceMethod(WKWebView.self, Selector.mediaMutedState), + let swizzledMethod = class_getInstanceMethod(WKWebView.self, #selector(getter: mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return false } - let mutedState: _WKMediaMutedState = { - guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } - return self._mediaMutedState() - }() - var newState = mutedState - - if newState == .audioMuted { - newState.remove(.audioMuted) - } else { - newState.insert(.audioMuted) + method_exchangeImplementations(originalMethod, swizzledMethod) + return true + }() + + static private let swizzleSetPageMutedOnce: Bool = { + guard let originalMethod = class_getInstanceMethod(WKWebView.self, Selector.setPageMuted), + let swizzledMethod = class_getInstanceMethod(WKWebView.self, #selector(setter: mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _setPageMuted:") + return false } - guard newState != mutedState else { return } - self._setPageMuted(newState) -#endif - } + method_exchangeImplementations(originalMethod, swizzledMethod) + return true + }() /// Returns the audio state of the WKWebView. /// /// - Returns: `muted` if the web view is muted /// `unmuted` if the web view is unmuted - /// `notSupported` if the web view does not support fetching the current audio state - func audioState() -> AudioState { -#if APPSTORE - return .notSupported -#else - guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { - assertionFailure("WKWebView does not respond to selector _mediaMutedState") - return .notSupported + var audioState: AudioState { + get { + AudioState(wkMediaMutedState: mediaMutedState) + } + set { + switch newValue { + case .muted: + self.mediaMutedState.insert(.audioMuted) + case .unmuted: + self.mediaMutedState.remove(.audioMuted) + } } - - let mutedState = self._mediaMutedState() - - return mutedState.contains(.audioMuted) ? .muted : .unmuted -#endif } func stopMediaCapture() { - guard #available(macOS 12.0, *) else { #if !APPSTORE + guard #available(macOS 12.0, *) else { guard self.responds(to: #selector(_stopMediaCapture)) else { assertionFailure("WKWebView does not respond to _stopMediaCapture") return } self._stopMediaCapture() -#endif return } +#endif setCameraCaptureState(.none) setMicrophoneCaptureState(.none) } func stopAllMediaPlayback() { - guard #available(macOS 12.0, *) else { #if !APPSTORE + guard #available(macOS 12.0, *) else { guard self.responds(to: #selector(_stopAllMediaPlayback)) else { assertionFailure("WKWebView does not respond to _stopAllMediaPlayback") return } self._stopAllMediaPlayback() return -#endif } +#endif pauseAllMediaPlayback() } @@ -212,20 +210,26 @@ extension WKWebView { switch permission { case .camera: guard #available(macOS 12.0, *) else { -#if !APPSTORE - self.setMediaCaptureMuted(muted) -#endif + if muted { + self.mediaMutedState.insert(.captureDevicesMuted) + } else { + self.mediaMutedState.remove(.captureDevicesMuted) + } return } + self.setCameraCaptureState(muted ? .muted : .active, completionHandler: {}) case .microphone: guard #available(macOS 12.0, *) else { -#if !APPSTORE - self.setMediaCaptureMuted(muted) -#endif + if muted { + self.mediaMutedState.insert(.captureDevicesMuted) + } else { + self.mediaMutedState.remove(.captureDevicesMuted) + } return } + self.setMicrophoneCaptureState(muted ? .muted : .active, completionHandler: {}) case .geolocation: self.configuration.processPool.geolocationProvider?.isPaused = muted @@ -360,6 +364,8 @@ extension WKWebView { static let fullScreenPlaceholderView = NSSelectorFromString("_fullScreenPlaceholderView") static let printOperationWithPrintInfoForFrame = NSSelectorFromString("_printOperationWithPrintInfo:forFrame:") static let loadAlternateHTMLString = NSSelectorFromString("_loadAlternateHTMLString:baseURL:forUnreachableURL:") + static let mediaMutedState = NSSelectorFromString("_mediaMutedState") + static let setPageMuted = NSSelectorFromString("_setPageMuted:") } } diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index ee5c9f0c91..7324862828 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -122,7 +122,7 @@ final class PinnedTabsViewModel: ObservableObject { audioStateView = .muted case .unmuted: audioStateView = .unmuted - case .notSupported: + case .none: audioStateView = .notSupported } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 40768d20a3..3e067f1187 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -212,7 +212,7 @@ struct PinnedTabInnerView: View { .renderingMode(.template) .frame(width: 12, height: 12) }.offset(x: 8, y: -8) - default: EmptyView() + case .unmuted, .none: EmptyView() } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index a1e4228051..39b66c9daa 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -539,7 +539,7 @@ protocol NewWindowPolicyDecisionMaker { self?.onDuckDuckGoEmailSignOut(notification) } - self.audioState = webView.audioState() + self.audioState = webView.audioState addDeallocationChecks(for: webView) } @@ -1035,12 +1035,11 @@ protocol NewWindowPolicyDecisionMaker { } } - @Published private(set) var audioState: WKWebView.AudioState = .notSupported + @Published private(set) var audioState: WKWebView.AudioState? func muteUnmuteTab() { - webView.muteOrUnmute() - - audioState = webView.audioState() + webView.audioState.toggle() + audioState = webView.audioState } private enum ReloadIfNeededSource { diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 6ebc31e640..8f0253224b 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1144,12 +1144,9 @@ extension TabBarViewController: TabBarViewItemDelegate { removeFireproofing(from: tab) } - func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState? { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), - let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] - else { - return .notSupported - } + let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] else { return nil } return tab.audioState } diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index b4a1bbca64..a92752aa56 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -46,7 +46,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemFireproofSite(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemRemoveFireproofing(_ tabBarViewItem: TabBarViewItem) - func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState? func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState @@ -446,7 +446,7 @@ final class TabBarViewItem: NSCollectionViewItem { switch delegate?.tabBarViewItemAudioState(self) { case .muted: mutedTabIcon.isHidden = false - default: + case .unmuted, .none: mutedTabIcon.isHidden = true } } @@ -540,15 +540,13 @@ extension TabBarViewItem: NSMenuDelegate { } private func addMuteUnmuteMenuItem(to menu: NSMenu) { - let audioState = delegate?.tabBarViewItemAudioState(self) ?? .notSupported - - if audioState != .notSupported { - menu.addItem(NSMenuItem.separator()) - let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab - let muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") - muteUnmuteMenuItem.target = self - menu.addItem(muteUnmuteMenuItem) - } + guard let audioState = delegate?.tabBarViewItemAudioState(self) else { return } + + menu.addItem(NSMenuItem.separator()) + let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab + let muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") + muteUnmuteMenuItem.target = self + menu.addItem(muteUnmuteMenuItem) } private func addCloseMenuItem(to menu: NSMenu) { diff --git a/UnitTests/Permissions/WebViewMock.swift b/UnitTests/Permissions/WebViewMock.swift index d8b0dd4ae5..8899c7f88f 100644 --- a/UnitTests/Permissions/WebViewMock.swift +++ b/UnitTests/Permissions/WebViewMock.swift @@ -107,15 +107,16 @@ final class WebViewMock: WKWebView { stopMediaCaptureHandler?() } - var mediaMutedStateValue = _WKMediaMutedState() - override func _mediaMutedState() -> _WKMediaMutedState { - mediaMutedStateValue - } - + var mediaMutedStateValue: _WKMediaMutedState = [] var setPageMutedHandler: ((_WKMediaMutedState) -> Void)? - override func _setPageMuted(_ mutedState: _WKMediaMutedState) { - mediaMutedStateValue = mutedState - setPageMutedHandler?(mutedState) + override var mediaMutedState: _WKMediaMutedState { + get { + mediaMutedStateValue + } + set { + mediaMutedStateValue = newValue + setPageMutedHandler?(newValue) + } } var setCameraCaptureStateHandler: ((Bool?) -> Void)? diff --git a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift index b569bdb58d..d62bf1f85a 100644 --- a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift +++ b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift @@ -39,6 +39,11 @@ final class WKWebViewPrivateMethodsAvailabilityTests: XCTestCase { XCTAssertTrue(WKBackForwardList.instancesRespond(to: WKBackForwardList.removeAllItemsSelector)) } + func testWebViewRespondsTo_pageMutedState() { + XCTAssertTrue(WKWebView.instancesRespond(to: WKWebView.Selector.setPageMuted)) + XCTAssertTrue(WKWebView.instancesRespond(to: WKWebView.Selector.mediaMutedState)) + } + func testWKWebpagePreferencesCustomHeaderFieldsSupported() { XCTAssertTrue(NavigationPreferences.customHeadersSupported) let testHeaders = ["X-CUSTOM-HEADER": "TEST"] diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index c09b956f23..e63dad9b0a 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -24,7 +24,7 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { var mockedCurrentTab: Tab? var hasItemsToTheRight = false - var audioState: WKWebView.AudioState = .notSupported + var audioState: WKWebView.AudioState? func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -86,7 +86,7 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } - func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState? { return audioState } @@ -99,7 +99,7 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } func clear() { - self.audioState = .notSupported + self.audioState = nil } } From d87a92e573369a8ad045c751127b807b40b165df Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 15 Apr 2024 15:06:19 +0000 Subject: [PATCH 088/221] Update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +- DuckDuckGo/ContentBlocker/macos-config.json | 370 +++++++----------- 2 files changed, 135 insertions(+), 239 deletions(-) diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index f0052639b2..504b70a61f 100644 --- a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"fd95ad4da437370f57ea8c2e2d03f48f\"" - public static let embeddedDataSHA = "f11d34eb516a2ba722c22e15ff8cdee5e5b2570adbf9d1b22d50438b30f57188" + public static let embeddedDataETag = "\"a482727f0d20b29eabd1e22fde2d54cf\"" + public static let embeddedDataSHA = "993aa84559944a8866e40cebbce02beee2b1597f86b63f998d000d2a0e5d617a" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 216cd4c618..7c20612900 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1712611145027, + "version": 1713140318814, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -74,12 +74,6 @@ { "domain": "thehustle.co" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -111,7 +105,7 @@ ] }, "state": "enabled", - "hash": "51b76aa7b92d78ad52106b04ac809843" + "hash": "16c6e3fb43797e3ca13a9259569a9e4e" }, "androidBrowserConfig": { "exceptions": [], @@ -285,12 +279,6 @@ { "domain": "condell-ltd.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -303,11 +291,12 @@ "generic-cosmetic", "termsfeed3", "strato.de", - "healthline-media" + "healthline-media", + "tarteaucitron.js" ] }, "state": "enabled", - "hash": "44af0b568856ce87b825bb7fc61b6961" + "hash": "0eaff8b64b6d3e8a59879f3b4ab6c0ba" }, "autofill": { "exceptions": [ @@ -923,12 +912,6 @@ { "domain": "pocketbook.digital" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -951,16 +934,10 @@ } }, "state": "disabled", - "hash": "36e8971fa9bb204b78a5929a14a108dd" + "hash": "770f7ae0f752e976764771bccec352b2" }, "clickToPlay": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -978,7 +955,7 @@ } }, "state": "enabled", - "hash": "f1b7de266435cd2e414f50deb2c9234a" + "hash": "2cff3d9b2df1ed9375f362848c8ed5f3" }, "clientBrandHint": { "exceptions": [], @@ -1009,12 +986,6 @@ { "domain": "soranews24.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -1022,7 +993,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "e37447d42ee8194f185e35e40f577f41" + "hash": "593797946074a1f304add65e7543b9be" }, "cookie": { "settings": { @@ -1064,12 +1035,6 @@ { "domain": "news.ti.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -1078,7 +1043,7 @@ } ], "state": "disabled", - "hash": "37a27966915571085613911b47e6e2eb" + "hash": "7ade754b885238cd191f1a61b4eeb0b6" }, "customUserAgent": { "settings": { @@ -1245,12 +1210,6 @@ { "domain": "duckduckgo.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -2300,6 +2259,15 @@ } ] }, + { + "domain": "doodle.com", + "rules": [ + { + "selector": "[data-testid*='ads-layout-placement']", + "type": "hide" + } + ] + }, { "domain": "dpreview.com", "rules": [ @@ -4050,6 +4018,15 @@ } ] }, + { + "domain": "woot.com", + "rules": [ + { + "selector": "[data-test-ui*='advertisementLeaderboard']", + "type": "hide-empty" + } + ] + }, { "domain": "wsj.com", "rules": [ @@ -4254,16 +4231,10 @@ ] }, "state": "enabled", - "hash": "ea31ebf0dd3e4831467ed2b2ec783279" + "hash": "c0fa0dfbc6231be31492023b623ac99b" }, "exceptionHandler": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4272,7 +4243,7 @@ } ], "state": "disabled", - "hash": "5e792dd491428702bc0104240fbce0ce" + "hash": "dc1b4fa301193a03ddcd4bdf7ee3e610" }, "fingerprintingAudio": { "state": "disabled", @@ -4280,12 +4251,6 @@ { "domain": "litebluesso.usps.gov" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4293,19 +4258,13 @@ "domain": "sundancecatalog.com" } ], - "hash": "f25a8f2709e865c2bd743828c7ee2f77" + "hash": "2037fcd805ece181cfffc482f262941f" }, "fingerprintingBattery": { "exceptions": [ { "domain": "litebluesso.usps.gov" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4314,7 +4273,7 @@ } ], "state": "disabled", - "hash": "4085f1593faff2feac2093533b819a41" + "hash": "07ee708dc740aab3b1f048d9bb571dac" }, "fingerprintingCanvas": { "settings": { @@ -4405,12 +4364,6 @@ { "domain": "godaddy.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4419,7 +4372,7 @@ } ], "state": "disabled", - "hash": "ea4c565bae27996f0d651300d757594c" + "hash": "d48bfb1151476f49970ffd3b1f778bf9" }, "fingerprintingHardware": { "settings": { @@ -4471,12 +4424,6 @@ { "domain": "proton.me" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4518,7 +4465,7 @@ } ], "state": "enabled", - "hash": "37e16df501e3e68416a13f991b4e4147" + "hash": "aefca8e7a9a3d9b65370608dd639cd3f" }, "fingerprintingScreenSize": { "settings": { @@ -4558,12 +4505,6 @@ { "domain": "secureserver.net" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4572,7 +4513,7 @@ } ], "state": "disabled", - "hash": "466b85680f138657de9bfd222c440887" + "hash": "eef4614273c28d50dd298a68ffbac309" }, "fingerprintingTemporaryStorage": { "exceptions": [ @@ -4585,12 +4526,6 @@ { "domain": "tattoogenius.art" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4599,16 +4534,10 @@ } ], "state": "disabled", - "hash": "f858697949c90842c450daee64a1dc30" + "hash": "2746e6cb6c773e80a36fda03618ef930" }, "googleRejected": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4617,7 +4546,7 @@ } ], "state": "disabled", - "hash": "5e792dd491428702bc0104240fbce0ce" + "hash": "dc1b4fa301193a03ddcd4bdf7ee3e610" }, "gpc": { "state": "enabled", @@ -4634,6 +4563,9 @@ { "domain": "crunchyroll.com" }, + { + "domain": "espn.com" + }, { "domain": "eventbrite.com" }, @@ -4652,12 +4584,6 @@ { "domain": "tirerack.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4674,7 +4600,7 @@ "privacy-test-pages.site" ] }, - "hash": "1a1373bcf16647d63220659fce650a83" + "hash": "05bddff3ae61a9536e38a6ef7d383eb3" }, "harmfulApis": { "settings": { @@ -4776,12 +4702,6 @@ "domains": [] }, "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4790,7 +4710,7 @@ } ], "state": "disabled", - "hash": "44d3e707cba3ee0a3578f52dc2ce2aa4" + "hash": "f29eae11500edcda80aa8b32b12869eb" }, "history": { "state": "disabled", @@ -4809,12 +4729,6 @@ { "domain": "jp.square-enix.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4822,7 +4736,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "f772808ed34cc9ea8cbcbb7cdaf74429" + "hash": "ea2c4fc84f27eb3694acd9ccf1023e95" }, "incontextSignup": { "exceptions": [], @@ -4859,12 +4773,6 @@ }, "navigatorInterface": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4880,7 +4788,7 @@ ] }, "state": "enabled", - "hash": "698de7b963d7d7942c5c5d1e986bb1b1" + "hash": "bb2ed420e76ecaf31a1f99decd370e55" }, "networkProtection": { "state": "enabled", @@ -4925,23 +4833,25 @@ "exceptions": [], "state": "enabled", "settings": { - "surveyCardDay0": "enabled", + "surveyCardDay0": "disabled", "surveyCardDay7": "disabled", - "surveyCardDay14": "enabled" + "surveyCardDay14": "disabled", + "permanentSurvey": { + "state": "internal", + "localization": "disabled", + "url": "https://selfserve.decipherinc.com/survey/selfserve/32ab/240404?list=2", + "firstDay": 5, + "lastDay": 8, + "sharePercentage": 60 + } }, - "hash": "eb826d9079211f30d624211f44aed184" + "hash": "7f7445d021268ef854b20022d0fb48e6" }, "nonTracking3pCookies": { "settings": { "excludedCookieDomains": [] }, "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4950,17 +4860,11 @@ } ], "state": "disabled", - "hash": "841fa92b9728c9754f050662678f82c7" + "hash": "522e3e42e2612ac2811342d3f6754c5a" }, "performanceMetrics": { "state": "enabled", "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -4968,7 +4872,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "38558d5e7b231d4b27e7dd76814387a7" + "hash": "936f00970c108fd646f73d00b3f3f5b5" }, "privacyDashboard": { "exceptions": [], @@ -4998,7 +4902,27 @@ "exceptions": [], "features": { "isLaunched": { - "state": "disabled" + "state": "enabled", + "rollout": { + "steps": [ + { + "percent": 1 + }, + { + "percent": 10 + }, + { + "percent": 30 + }, + { + "percent": 50 + }, + { + "percent": 100 + } + ] + }, + "minSupportedVersion": "1.82.1" }, "isLaunchedOverride": { "state": "disabled" @@ -5007,13 +4931,33 @@ "state": "enabled" }, "isLaunchedStripe": { - "state": "disabled" + "state": "enabled", + "rollout": { + "steps": [ + { + "percent": 1 + }, + { + "percent": 10 + }, + { + "percent": 30 + }, + { + "percent": 50 + }, + { + "percent": 100 + } + ] + }, + "minSupportedVersion": "1.82.1" }, "allowPurchaseStripe": { "state": "enabled" } }, - "hash": "c9153c5cc3b6b7eba024c6c597e15edb" + "hash": "1a10a68ea8885db0d1d9bab371c0f738" }, "privacyProtectionsPopup": { "state": "disabled", @@ -5040,12 +4984,6 @@ { "domain": "xcelenergy.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -5054,17 +4992,11 @@ } ], "state": "disabled", - "hash": "0d3df0f7c24ebde89d2dced4e2d34322" + "hash": "1cfe449de8a4fcb542757383d846c031" }, "requestFilterer": { "state": "disabled", "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -5075,17 +5007,11 @@ "settings": { "windowInMs": 0 }, - "hash": "0fff8017d8ea4b5609b8f5c110be1401" + "hash": "3218faa098a2e9da61447fb63e3a5ed9" }, "runtimeChecks": { "state": "disabled", "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -5094,16 +5020,10 @@ } ], "settings": {}, - "hash": "800a19533c728bbec7e31e466f898268" + "hash": "ddb64344aa42e9593964f14b6de3d6df" }, "serviceworkerInitiatedRequests": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -5112,7 +5032,7 @@ } ], "state": "disabled", - "hash": "5e792dd491428702bc0104240fbce0ce" + "hash": "dc1b4fa301193a03ddcd4bdf7ee3e610" }, "sslCertificates": { "state": "enabled", @@ -5569,6 +5489,12 @@ }, "boldapps.net": { "rules": [ + { + "rule": "mc.boldapps.net/install_assets/bold.multicurrency.js", + "domains": [ + "" + ] + }, { "rule": "option.boldapps.net/js/options.js", "domains": [ @@ -6061,11 +5987,7 @@ { "rule": "cdn.dynamicyield.com/api/", "domains": [ - "asics.com", - "brooklinen.com", - "carters.com", - "otterbox.com", - "seatosummit.com" + "" ] } ] @@ -7003,6 +6925,16 @@ } ] }, + "litix.io": { + "rules": [ + { + "rule": "src.litix.io/videojs/", + "domains": [ + "" + ] + } + ] + }, "loggly.com": { "rules": [ { @@ -7984,6 +7916,12 @@ "domains": [ "winnipegfreepress.com" ] + }, + { + "rule": "platform.twitter.com/_next/static", + "domains": [ + "" + ] } ] }, @@ -8250,12 +8188,6 @@ } }, "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8263,7 +8195,7 @@ "domain": "sundancecatalog.com" } ], - "hash": "936913b03c62ec1861b64a7a2316ddfd" + "hash": "2d627140b59bca8b8edbc236e79cd46e" }, "trackingCookies1p": { "settings": { @@ -8273,12 +8205,6 @@ } }, "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8287,19 +8213,13 @@ } ], "state": "disabled", - "hash": "4dddf681372a2aea9788090b13db6e6f" + "hash": "dab51f5ad2454727f8bc474cdd3da65b" }, "trackingCookies3p": { "settings": { "excludedCookieDomains": [] }, "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8308,19 +8228,13 @@ } ], "state": "disabled", - "hash": "841fa92b9728c9754f050662678f82c7" + "hash": "522e3e42e2612ac2811342d3f6754c5a" }, "trackingParameters": { "exceptions": [ { "domain": "axs.com" }, - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8359,19 +8273,13 @@ }, "state": "enabled", "minSupportedVersion": "0.22.3", - "hash": "9a0282376084874f0245c421d6943841" + "hash": "44005192b6dba245e95de042fc224228" }, "userAgentRotation": { "settings": { "agentExcludePatterns": [] }, "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8380,7 +8288,7 @@ } ], "state": "disabled", - "hash": "f65d10dfdf6739feab99a08d42734747" + "hash": "e30277704ddbf20c14136baab08519c5" }, "voiceSearch": { "exceptions": [], @@ -8389,12 +8297,6 @@ }, "webCompat": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8663,7 +8565,7 @@ } ] }, - "hash": "151d7ee40451c4aac4badfcc829ea0b5" + "hash": "90687b1a5ac7aec8bf26f24a75cd883f" }, "windowsPermissionUsage": { "exceptions": [], @@ -8672,12 +8574,6 @@ }, "windowsStartupBoost": { "exceptions": [ - { - "domain": "earth.google.com" - }, - { - "domain": "iscorp.com" - }, { "domain": "marvel.com" }, @@ -8686,7 +8582,7 @@ } ], "state": "disabled", - "hash": "5e792dd491428702bc0104240fbce0ce" + "hash": "dc1b4fa301193a03ddcd4bdf7ee3e610" }, "windowsWaitlist": { "exceptions": [], From bcc8f01c5f74260e8d44ac08e2b76f5107d08724 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 15 Apr 2024 15:06:19 +0000 Subject: [PATCH 089/221] Set marketing version to 1.84.0 --- Configuration/Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 71f4ed0cad..1e0572489f 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 1.83.0 +MARKETING_VERSION = 1.84.0 From 4a357fc468616deaa70508e32faa382eb2722a4b Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Mon, 15 Apr 2024 15:18:06 +0000 Subject: [PATCH 090/221] Bump version to 1.84.0 (163) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index 1155c5e451..5b507f55dc 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 162 +CURRENT_PROJECT_VERSION = 163 From 6a3c17f2273745a0ffffb4939feaa92e3112ec92 Mon Sep 17 00:00:00 2001 From: Daniel Bernal Date: Mon, 15 Apr 2024 17:29:33 +0200 Subject: [PATCH 091/221] Limit stale marks/actions to be PRs only (#2627) Limit PR stale marking actions to be PRs only. (Avoid marking issues) --- .github/workflows/stale_pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml index a5590c9c63..a4a6e92c29 100644 --- a/.github/workflows/stale_pr.yml +++ b/.github/workflows/stale_pr.yml @@ -12,8 +12,8 @@ jobs: uses: actions/stale@v9 with: stale-pr-message: 'This PR has been inactive for more than 7 days and will be automatically closed 7 days from now.' - days-before-stale: 7 + days-before-pr-stale: 7 close-pr-message: 'This PR has been closed after 14 days of inactivity. Feel free to reopen it if you plan to continue working on it or have further discussions.' - days-before-close: 7 + days-before-pr-close: 7 stale-pr-label: stale exempt-draft-pr: true \ No newline at end of file From 9aaa05ea311812c05c278f644547762aa55ff148 Mon Sep 17 00:00:00 2001 From: Halle <378795+Halle@users.noreply.github.com> Date: Mon, 15 Apr 2024 11:04:19 -0700 Subject: [PATCH 092/221] UI Tests: Permissions (#2625) Task/Issue URL: https://app.asana.com/0/1199230911884351/1205717021705373/f Description: Adds a series of UI tests for permissions --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../AddressBarButtonsViewController.swift | 1 + .../View/NavigationBar.storyboard | 4 + ...ermissionAuthorizationViewController.swift | 2 + .../Permissions/View/PermissionButton.swift | 5 + .../View/PermissionContextMenu.swift | 12 +- .../AddressBarKeyboardShortcutsTests.swift | 1 + UITests/AutocompleteTests.swift | 1 + UITests/BookmarksAndFavoritesTests.swift | 7 +- UITests/BookmarksBarTests.swift | 4 + UITests/BrowsingHistoryTests.swift | 4 + UITests/Common/UITests.swift | 13 + UITests/Common/XCUIElementExtension.swift | 14 + UITests/FindInPageTests.swift | 41 +- UITests/PermissionsTests.swift | 595 ++++++++++++++++++ UITests/StateRestorationTests.swift | 4 + UITests/TabBarTests.swift | 4 + 17 files changed, 693 insertions(+), 23 deletions(-) create mode 100644 UITests/PermissionsTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a97d747dd3..a817ecdd4a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3320,6 +3320,7 @@ EE339228291BDEFD009F62C1 /* JSAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE339227291BDEFD009F62C1 /* JSAlertController.swift */; }; EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE42CBCC2BC8004700AD411C /* PermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */; }; EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */; }; EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; @@ -4813,6 +4814,7 @@ EE0429DF2BA31D2F009EB20F /* FindInPageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindInPageTests.swift; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; + EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionsTests.swift; sourceTree = ""; }; EE54F7B22BBFEA48006218DB /* BookmarksAndFavoritesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksAndFavoritesTests.swift; sourceTree = ""; }; EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; @@ -6614,6 +6616,7 @@ EE7F74902BB5D76600CD9456 /* BookmarksBarTests.swift */, EE02D41B2BB460A600DBE6B3 /* BrowsingHistoryTests.swift */, EE0429DF2BA31D2F009EB20F /* FindInPageTests.swift */, + EE42CBCB2BC8004700AD411C /* PermissionsTests.swift */, EE9D81C22BC57A3700338BE3 /* StateRestorationTests.swift */, 7B4CE8E626F02134009134B1 /* TabBarTests.swift */, ); @@ -12363,6 +12366,7 @@ EEC7BE2E2BC6C09500F86835 /* AddressBarKeyboardShortcutsTests.swift in Sources */, EE54F7B32BBFEA49006218DB /* BookmarksAndFavoritesTests.swift in Sources */, EE02D4222BB4611A00DBE6B3 /* TestsURLExtension.swift in Sources */, + EE42CBCC2BC8004700AD411C /* PermissionsTests.swift in Sources */, 7B4CE8E726F02135009134B1 /* TabBarTests.swift in Sources */, EEBCE6832BA463DD00B9DF00 /* NSImageExtensions.swift in Sources */, EEBCE6822BA444FA00B9DF00 /* XCUIElementExtension.swift in Sources */, diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index e17e162d79..b6cf9fd9f0 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -49,6 +49,7 @@ final class AddressBarButtonsViewController: NSViewController { return permissionAuthorizationPopover ?? { let popover = PermissionAuthorizationPopover() self.permissionAuthorizationPopover = popover + popover.setAccessibilityIdentifier("AddressBarButtonsViewController.permissionAuthorizationPopover") return popover }() } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index 357dbe0d25..a656539f29 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -724,6 +724,7 @@ +