From a785c70e74fe6cf95271311c3b786ee8db5569d7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 21 Feb 2022 14:00:39 +0700 Subject: [PATCH 1/4] Fix video fullscreen exit crash (#295) * Fix crash on fullscreen exit * fix resizing * trying to fix Xcode 13.0 compatibility * Xcode 13.1 fix * Unit tests fixed for macOS 12 * fix tab switching * Fix window closing when video playing in full screen * cleanup Co-authored-by: Tomas Strba --- .../View/BrowserTabViewController.swift | 54 ++++++++++++++----- DuckDuckGo/BrowserTab/View/WebView.swift | 9 ++++ DuckDuckGo/Main/View/MainViewController.swift | 2 +- DuckDuckGo/Statistics/PixelEvent.swift | 2 +- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift b/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift index e862d6bfae..ff1d66886d 100644 --- a/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift @@ -31,7 +31,8 @@ final class BrowserTabViewController: NSViewController { @IBOutlet weak var errorMessageLabel: NSTextField! @IBOutlet weak var hoverLabel: NSTextField! @IBOutlet weak var hoverLabelContainer: NSView! - weak var webView: WebView? + private weak var webView: WebView? + private weak var webViewContainer: NSView? var tabViewModel: TabViewModel? @@ -76,6 +77,18 @@ final class BrowserTabViewController: NSViewController { subscribeToErrorViewState() } + override func viewDidAppear() { + NotificationCenter.default.addObserver(self, + selector: #selector(windowWillClose(_:)), + name: NSWindow.willCloseNotification, + object: self.view.window) + } + + @objc + private func windowWillClose(_ notification: NSNotification) { + self.removeWebViewFromHierarchy() + } + private func subscribeToSelectedTabViewModel() { selectedTabViewModelCancellable = tabCollectionViewModel.$selectedTabViewModel .sink { [weak self] selectedTabViewModel in @@ -100,6 +113,18 @@ final class BrowserTabViewController: NSViewController { show(tabContent: tabViewModel?.tab.content) } + private func removeWebViewFromHierarchy(webView: WebView? = nil, + container: NSView? = nil) { + guard let webView = webView ?? self.webView, + let container = container ?? self.webViewContainer + else { return } + + // close fullscreenWindowController when closing tab in FullScreen mode + webView.fullscreenWindowController?.close() + webView.removeFromSuperview() + container.removeFromSuperview() + } + private func addWebViewToViewHierarchy(_ webView: WebView) { // This code should ideally use Auto Layout, but in order to enable the web inspector, it needs to use springs & structs. // The line at the bottom of this comment is the "correct" method of doing this, but breaks the inspector. @@ -109,7 +134,12 @@ final class BrowserTabViewController: NSViewController { webView.frame = view.bounds webView.autoresizingMask = [.width, .height] - view.addSubview(webView) + + let container = NSView(frame: view.bounds) + container.autoresizingMask = [.width, .height] + view.addSubview(container) + container.addSubview(webView) + self.webViewContainer = container // Make sure this is on top view.addSubview(hoverLabelContainer) @@ -127,25 +157,20 @@ final class BrowserTabViewController: NSViewController { addWebViewToViewHierarchy(newWebView) } - func removeOldWebView(_ oldWebView: WebView?) { - if let oldWebView = oldWebView, view.subviews.contains(oldWebView) { - oldWebView.removeFromSuperview() - } - } - guard let tabViewModel = tabViewModel else { self.tabViewModel = nil - removeOldWebView(webView) + removeWebViewFromHierarchy() return } guard self.tabViewModel !== tabViewModel else { return } let oldWebView = webView + let webViewContainer = webViewContainer displayWebView(of: tabViewModel) subscribeToUrl(of: tabViewModel) self.tabViewModel = tabViewModel - removeOldWebView(oldWebView) + removeWebViewFromHierarchy(webView: oldWebView, container: webViewContainer) } func subscribeToUrl(of tabViewModel: TabViewModel) { @@ -167,13 +192,17 @@ final class BrowserTabViewController: NSViewController { } } + func makeWebViewFirstResponder() { + self.webView?.makeMeFirstResponder() + } + private func setFirstResponderIfNeeded() { guard webView?.url != nil else { return } DispatchQueue.main.async { [weak self] in - self?.webView?.makeMeFirstResponder() + self?.makeWebViewFirstResponder() } } @@ -229,7 +258,7 @@ final class BrowserTabViewController: NSViewController { preferencesViewController.removeCompletely() bookmarksViewController.removeCompletely() if includingWebView { - self.webView?.removeFromSuperview() + self.removeWebViewFromHierarchy() } } @@ -373,7 +402,6 @@ extension BrowserTabViewController: TabDelegate { func closeTab(_ tab: Tab) { guard let index = tabCollectionViewModel.tabCollection.tabs.firstIndex(of: tab) else { - return } tabCollectionViewModel.remove(at: index) diff --git a/DuckDuckGo/BrowserTab/View/WebView.swift b/DuckDuckGo/BrowserTab/View/WebView.swift index 61cbf039e6..9a85ca0114 100644 --- a/DuckDuckGo/BrowserTab/View/WebView.swift +++ b/DuckDuckGo/BrowserTab/View/WebView.swift @@ -170,4 +170,13 @@ final class WebView: WKWebView { inspectorPerform("showResources") } + var fullscreenWindowController: NSWindowController? { + guard let fullscreenWindowController = self.window?.windowController, + fullscreenWindowController.className.contains("FullScreen") + else { + return nil + } + return fullscreenWindowController + } + } diff --git a/DuckDuckGo/Main/View/MainViewController.swift b/DuckDuckGo/Main/View/MainViewController.swift index c45278a39c..d59ac60e0c 100644 --- a/DuckDuckGo/Main/View/MainViewController.swift +++ b/DuckDuckGo/Main/View/MainViewController.swift @@ -267,7 +267,7 @@ final class MainViewController: NSViewController { switch selectedTabViewModel.tab.content { case .homepage, .onboarding, .none: navigationBarViewController.addressBarViewController?.addressBarTextField.makeMeFirstResponder() case .url: - browserTabViewController.webView?.makeMeFirstResponder() + browserTabViewController.makeWebViewFirstResponder() case .preferences: browserTabViewController.preferencesViewController.view.makeMeFirstResponder() case .bookmarks: browserTabViewController.bookmarksViewController.view.makeMeFirstResponder() } diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 0892f66820..f50a8e0386 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -55,7 +55,7 @@ extension Pixel { } case navigation(kind: NavigationKind, source: NavigationAccessPoint) - + case serp case suggestionsDisplayed(hasBookmark: HasBookmark, hasFavorite: HasFavorite, hasHistoryEntry: HasHistoryEntry) From ca4141280317c3f836a027aca0047efc8d6e6bb3 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Mon, 21 Feb 2022 10:41:40 -0600 Subject: [PATCH 2/4] Pass config data to Autofill UserScript (#418) * Pass config data to Autofill UserScript * Update BSK * Update BSK * Update embedded config hash * Fix dashboard commit * Adopt Autofill source provider * Lint fix * Update BSK reference * Update BSK reference * Update config * Update etag and hash --- DuckDuckGo.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- DuckDuckGo/BrowserTab/Model/UserScripts.swift | 3 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- .../ScriptSourceProviding.swift | 10 + DuckDuckGo/ContentBlocker/macos-config.json | 283 +++++++++++++++--- 6 files changed, 266 insertions(+), 42 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8b4e84e954..23b44f4538 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5342,8 +5342,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 8.0.2; + kind = upToNextMajorVersion; + minimumVersion = 9.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2103a63075..356c96cf63 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/duckduckgo/BrowserServicesKit", "state": { "branch": null, - "revision": "37cb952b7d6b13b4bd41261031c5fa03b421aae6", - "version": "8.0.2" + "revision": "a263b2f9557ecfb6b09c5daac9cebd65ddfec6a4", + "version": "9.0.0" } }, { diff --git a/DuckDuckGo/BrowserTab/Model/UserScripts.swift b/DuckDuckGo/BrowserTab/Model/UserScripts.swift index 09eb579206..46993a6871 100644 --- a/DuckDuckGo/BrowserTab/Model/UserScripts.swift +++ b/DuckDuckGo/BrowserTab/Model/UserScripts.swift @@ -29,12 +29,12 @@ final class UserScripts { let printingUserScript = PrintingUserScript() let hoverUserScript = HoverUserScript() let debugScript = DebugUserScript() - let autofillScript = AutofillUserScript() let clickToLoadScript = ClickToLoadUserScript() let contentBlockerRulesScript: ContentBlockerRulesUserScript let surrogatesScript: SurrogatesUserScript let contentScopeUserScript: ContentScopeUserScript + let autofillScript: AutofillUserScript init(with sourceProvider: ScriptSourceProviding) { @@ -44,6 +44,7 @@ final class UserScripts { let sessionKey = sourceProvider.sessionKey ?? "" let prefs = ContentScopeProperties.init(gpcEnabled: privacySettings.gpcEnabled, sessionKey: sessionKey) contentScopeUserScript = ContentScopeUserScript(sourceProvider.privacyConfigurationManager, properties: prefs) + autofillScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider!) } lazy var userScripts: [UserScript] = [ diff --git a/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift index cbb2e34a42..6fc80e9035 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 embeddedConfigETag = "083fe0926381273459e458bcca3e7c9a" - public static let embeddedConfigurationSHA = "9c09f3b3064024bac710e03ddd0b625ed73bd4d46d4fc75fa04983dfd5fc1239" + public static let embeddedConfigETag = "a18405692732951d628f86b7ba1a1e1d" + public static let embeddedConfigurationSHA = "ef78422d8a7b1a324d6cbdb70b3605eca4934aa0b090a25548e51e42cffda53e" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index 94be50b772..dee7f47730 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -26,6 +26,7 @@ protocol ScriptSourceProviding { var contentBlockerRulesConfig: ContentBlockerUserScriptConfig? { get } var surrogatesConfig: SurrogatesUserScriptConfig? { get } var privacyConfigurationManager: PrivacyConfigurationManager { get } + var autofillSourceProvider: AutofillUserScriptSourceProvider? { get } var sessionKey: String? { get } var clickToLoadSource: String { get } @@ -39,6 +40,7 @@ final class DefaultScriptSourceProvider: ScriptSourceProviding { private(set) var contentBlockerRulesConfig: ContentBlockerUserScriptConfig? private(set) var surrogatesConfig: SurrogatesUserScriptConfig? + private(set) var autofillSourceProvider: AutofillUserScriptSourceProvider? private(set) var sessionKey: String? private(set) var clickToLoadSource: String = "" @@ -80,12 +82,20 @@ final class DefaultScriptSourceProvider: ScriptSourceProviding { surrogatesConfig = buildSurrogatesConfig() sessionKey = generateSessionKey() clickToLoadSource = buildClickToLoadSource() + autofillSourceProvider = buildAutofillSource() sourceUpdatedSubject.send( knownChanges ) } private func generateSessionKey() -> String { return UUID().uuidString } + + private func buildAutofillSource() -> AutofillUserScriptSourceProvider { + let privacySettings = PrivacySecurityPreferences() + return DefaultAutofillSourceProvider(privacyConfigurationManager: self.privacyConfigurationManager, + properties: ContentScopeProperties(gpcEnabled: privacySettings.gpcEnabled, + sessionKey: self.sessionKey ?? "")) + } private func buildContentBlockerRulesConfig() -> ContentBlockerUserScriptConfig { diff --git a/DuckDuckGo/ContentBlocker/macos-config.json b/DuckDuckGo/ContentBlocker/macos-config.json index 86c03aa27c..003e559d50 100644 --- a/DuckDuckGo/ContentBlocker/macos-config.json +++ b/DuckDuckGo/ContentBlocker/macos-config.json @@ -1,9 +1,33 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1637856403667, + "version": 1645459859348, "features": { + "ampLinks": { + "exceptions": [], + "settings": { + "linkFormats": [ + "^https?:\\/\\/(?:w{3}\\.)?google\\.\\S{2,}\\/amp\\/s\\/(\\S+)$", + "^https?:\\/\\/\\S+ampproject\\.org\\/v\\/s\\/(\\S+)$" + ], + "keywords": [ + "=amp", + "amp=", + "&", + "amp&", + "/amp", + "amp/", + ".amp", + "amp.", + "%amp", + "amp%", + "_amp", + "amp_", + "?amp" + ] + }, + "state": "disabled" + }, "autoconsent": { - "state": "enabled", "exceptions": [ { "domain": "spiegel.de", @@ -12,7 +36,8 @@ ], "settings": { "disabledCMPs": [] - } + }, + "state": "enabled" }, "autofill": { "exceptions": [ @@ -25,15 +50,27 @@ "reason": "site breakage" } ], - "state": "disabled" + "state": "enabled" }, "clickToPlay": { - "state": "enabled", - "exceptions": [] + "exceptions": [], + "state": "enabled" }, "contentBlocking": { "state": "enabled", "exceptions": [ + { + "domain": "pureflix.com", + "reason": "Videos not playing" + }, + { + "domain": "www.omahasteaks.com", + "reason": "Add to cart not working" + }, + { + "domain": "iltalehti.fi", + "reason": "Video content missing" + }, { "domain": "agame.com", "reason": "Site not loading" @@ -50,10 +87,6 @@ "domain": "ah.nl", "reason": "Missing site content" }, - { - "domain": "toledoblade.com", - "reason": "Missing site content" - }, { "domain": "magicgameworld.com", "reason": "Adblocker wall" @@ -113,6 +146,10 @@ { "domain": "virginmedia.com", "reason": "Chat widget blocked" + }, + { + "domain": "fantasynamegenerators.com", + "reason": "Site not loading" } ] }, @@ -130,11 +167,10 @@ ] }, "fingerprintingBattery": { - "state": "enabled", - "exceptions": [] + "exceptions": [], + "state": "disabled" }, "fingerprintingCanvas": { - "state": "enabled", "exceptions": [ { "domain": "walgreens.com", @@ -156,10 +192,10 @@ "domain": "kroger.com", "reason": "Broken login" } - ] + ], + "state": "disabled" }, "fingerprintingHardware": { - "state": "enabled", "exceptions": [ { "domain": "play.geforcenow.com", @@ -169,19 +205,20 @@ "domain": "stadia.google.com", "reason": "site breakage" } - ] + ], + "state": "disabled" }, "fingerprintingScreenSize": { - "state": "enabled", - "exceptions": [] + "exceptions": [], + "state": "disabled" }, "fingerprintingTemporaryStorage": { - "state": "enabled", - "exceptions": [] + "exceptions": [], + "state": "disabled" }, "floc": { - "state": "enabled", - "exceptions": [] + "exceptions": [], + "state": "disabled" }, "gpc": { "state": "enabled", @@ -194,6 +231,7 @@ "settings": { "gpcHeaderEnabledSites": [ "global-privacy-control.glitch.me", + "globalprivacycontrol.org", "washingtonpost.com", "nytimes.com", "privacy-test-pages.glitch.me" @@ -204,14 +242,63 @@ "state": "enabled", "exceptions": [] }, + "navigatorCredentials": { + "exceptions": [], + "state": "enabled" + }, + "navigatorInterface": { + "exceptions": [], + "state": "enabled" + }, "referrer": { - "state": "enabled", - "exceptions": [] + "exceptions": [], + "state": "disabled" }, "trackerAllowlist": { "state": "enabled", "settings": { "allowlistedTrackers": { + "amazon-adsystem.com": { + "rules": [ + { + "rule": "c.amazon-adsystem.com/aax2/apstag.js", + "domains": [ + "seattletimes.com" + ], + "reason": "adwall" + } + ] + }, + "cloudflare.com": { + "rules": [ + { + "rule": "cdnjs.cloudflare.com/ajax/libs/fingerprintjs2/1.8.6/fingerprint2.min.js", + "domains": [ + "winnipegfreepress.com" + ], + "reason": "content not loading" + } + ] + }, + "doubleclick.net": { + "rules": [ + { + "rule": "securepubads.g.doubleclick.net/tag/js/gpt.js", + "domains": [ + "wunderground.com", + "youmath.it" + ], + "reason": "videos not loading, adwall" + }, + { + "rule": "securepubads.g.doubleclick.net/gpt/pubads_impl_", + "domains": [ + "wunderground.com" + ], + "reason": "videos not loading" + } + ] + }, "dynamicyield.com": { "rules": [ { @@ -222,23 +309,123 @@ "reason": "checkout broken" } ] + }, + "evidon.com": { + "rules": [ + { + "rule": "c.evidon.com/geo/country.js", + "domains": [ + "crunchyroll.com" + ], + "reason": "video not loading" + }, + { + "rule": "evidon.com/sitenotice/", + "domains": [ + "crunchyroll.com" + ], + "reason": "video not loading" + } + ] + }, + "googlesyndication.com": { + "rules": [ + { + "rule": "pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", + "domains": [ + "youmath.it" + ], + "reason": "adwall" + }, + { + "rule": "tpc.googlesyndication.com/pagead/js/loader21.html", + "domains": [ + "rumble.com" + ], + "reason": "adwall (video blocked)" + } + ] + }, + "googletagmanager.com": { + "rules": [ + { + "rule": "googletagmanager.com/gtm.js", + "domains": [ + "rbcroyalbank.com" + ], + "reason": "page not loading" + } + ] + }, + "shopeemobile.com": { + "rules": [ + { + "rule": "deo.shopeemobile.com/shopee/shopee-pcmall-live-sg//assets/2581.e008c4d7fc4804de8ba6.js", + "domains": [ + "shopee.co.id", + "shopee.co.th", + "shopee.com.br", + "shopee.com.my", + "shopee.ph", + "shopee.sg", + "shopee.tw", + "shopee.vn" + ], + "reason": "content not loading" + } + ] + }, + "theplatform.com": { + "rules": [ + { + "rule": "pdk.theplatform.com/5.9.6/pdk/tpPdk.js", + "domains": [ + "bravotv.com", + "oxygen.com" + ], + "reason": "video not loading" + } + ] + }, + "twitter.com": { + "rules": [ + { + "rule": "platform.twitter.com/embed/embed.modules", + "domains": [ + "notthebee.com", + "upworthy.com" + ], + "reason": "content not rendering" + }, + { + "rule": "platform.twitter.com/widgets/tweet_button", + "domains": [ + "winnipegfreepress.com" + ], + "reason": "missing tweet button" + } + ] } } }, "exceptions": [] }, "trackingCookies1p": { - "state": "enabled", "settings": { "firstPartyTrackerCookiePolicy": { "threshold": 86400, "maxAge": 86400 } }, - "exceptions": [] + "exceptions": [ + { + "domain": "nespresso.com", + "reason": "login issues" + } + ], + "state": "disabled" }, "trackingCookies3p": { - "state": "enabled", "settings": { "excludedCookieDomains": [ { @@ -347,10 +534,39 @@ } ] }, - "exceptions": [] + "exceptions": [], + "state": "disabled" + }, + "trackingParameters": { + "exceptions": [], + "settings": { + "parameters": [ + "utm_[a-zA-Z]*", + "gclid", + "fbclid", + "fb_action_ids", + "fb_action_types", + "fb_source", + "fb_ref", + "ga_source", + "ga_medium", + "ga_term", + "ga_content", + "ga_campaign", + "ga_place", + "action_object_map", + "action_type_map", + "action_ref_map", + "gs_l", + "mkt_tok", + "hmb_campaign", + "hmb_source", + "hmb_medium" + ] + }, + "state": "disabled" }, "userAgentRotation": { - "state": "disabled", "settings": { "agentExcludePatterns": [ { @@ -396,11 +612,8 @@ "domain": "dzcdn.net", "reason": "Breaks images on deezer" } - ] - }, - "navigatorCredentials": { - "state": "enabled", - "exceptions": [] + ], + "state": "disabled" } }, "unprotectedTemporary": [ @@ -425,4 +638,4 @@ "reason": "site breakage" } ] -} \ No newline at end of file +} From e0345e7565070e0358903e0281501e084dc96235 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Tue, 22 Feb 2022 11:25:46 +0100 Subject: [PATCH 3/4] Cookie prompt management (#312) * Consent popup management feature Increase test timeout for CI Updated cookie-consent copy. Fix error when trying to run an action on a closed tab. Break autoconsent popover message Make autoconsent popover open underneath the urlbar. Move preferences scrollbar to the edge of the window Move cookie settings above GPC. Update copy * Another copy tweak. * Code tidying. * Extract protocol for autoconsent cache clear. * Final copy --- DuckDuckGo.xcodeproj/project.pbxproj | 64 + .../Autoconsent/AutoconsentBackground.swift | 290 ++ .../Autoconsent/AutoconsentUserScript.swift | 199 + DuckDuckGo/Autoconsent/autoconsent-bundle.js | 487 ++ DuckDuckGo/Autoconsent/autoconsent.html | 26 + DuckDuckGo/Autoconsent/background-bundle.js | 3977 +++++++++++++++++ DuckDuckGo/Autoconsent/background.js | 83 + DuckDuckGo/Autoconsent/browser-shim.js | 77 + DuckDuckGo/Autoconsent/userscript.js | 27 + DuckDuckGo/BrowserTab/Model/Tab.swift | 10 +- DuckDuckGo/BrowserTab/Model/UserScripts.swift | 7 + .../Common/Extensions/NSAlertExtension.swift | 10 + DuckDuckGo/Common/Localizables/UserText.swift | 7 + DuckDuckGo/Common/Utilities/Logging.swift | 7 + .../Utilities/UserDefaultsWrapper.swift | 1 + DuckDuckGo/Fire/Model/Fire.swift | 21 +- .../View/NavigationBarViewController.swift | 21 + .../Model/PrivacySecurityPreferences.swift | 7 + .../Preferences/View/Preferences.storyboard | 10 +- .../View/PreferencesListViewController.swift | 10 +- ...vacySecurityPreferencesTableCellView.swift | 15 +- ...rivacySecurityPreferencesTableCellView.xib | 73 +- .../Model/CookieConsentInfo.swift | 25 + .../Model/PrivacyDashboardUserScript.swift | 8 + .../View/PrivacyDashboardViewController.swift | 11 + DuckDuckGo/Statistics/PixelEvent.swift | 8 + DuckDuckGo/Statistics/PixelParameters.swift | 4 +- .../AutoconsentBackgroundTests.swift | 87 + Integration Tests/autoconsent-test-page.html | 34 + Integration Tests/autoconsent-test.js | 18 + Submodules/duckduckgo-privacy-dashboard | 2 +- package-lock.json | 154 + package.json | 11 +- rollup.config.js | 27 + tsconfig.json | 11 + 35 files changed, 5795 insertions(+), 34 deletions(-) create mode 100644 DuckDuckGo/Autoconsent/AutoconsentBackground.swift create mode 100644 DuckDuckGo/Autoconsent/AutoconsentUserScript.swift create mode 100644 DuckDuckGo/Autoconsent/autoconsent-bundle.js create mode 100644 DuckDuckGo/Autoconsent/autoconsent.html create mode 100644 DuckDuckGo/Autoconsent/background-bundle.js create mode 100644 DuckDuckGo/Autoconsent/background.js create mode 100644 DuckDuckGo/Autoconsent/browser-shim.js create mode 100644 DuckDuckGo/Autoconsent/userscript.js create mode 100644 DuckDuckGo/Privacy Dashboard/Model/CookieConsentInfo.swift create mode 100644 Integration Tests/Autoconsent/AutoconsentBackgroundTests.swift create mode 100644 Integration Tests/autoconsent-test-page.html create mode 100644 Integration Tests/autoconsent-test.js create mode 100644 package-lock.json create mode 100644 rollup.config.js create mode 100644 tsconfig.json diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 23b44f4538..3b3067ff8e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -457,6 +457,18 @@ AAF7D3862567CED500998667 /* WebViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF7D3852567CED500998667 /* WebViewConfiguration.swift */; }; AAFCB37F25E545D400859DD4 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */; }; AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */; }; + B31055C427A1BA1D001AC618 /* AutoconsentUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31055BC27A1BA1D001AC618 /* AutoconsentUserScript.swift */; }; + B31055C527A1BA1D001AC618 /* autoconsent.html in Resources */ = {isa = PBXBuildFile; fileRef = B31055BD27A1BA1D001AC618 /* autoconsent.html */; }; + B31055C627A1BA1D001AC618 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; + B31055C727A1BA1D001AC618 /* browser-shim.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BF27A1BA1D001AC618 /* browser-shim.js */; }; + B31055C827A1BA1D001AC618 /* background-bundle.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055C027A1BA1D001AC618 /* background-bundle.js */; }; + B31055C927A1BA1D001AC618 /* AutoconsentBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31055C127A1BA1D001AC618 /* AutoconsentBackground.swift */; }; + B31055CA27A1BA1D001AC618 /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055C227A1BA1D001AC618 /* background.js */; }; + B31055CB27A1BA1D001AC618 /* autoconsent-bundle.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055C327A1BA1D001AC618 /* autoconsent-bundle.js */; }; + B31055CE27A1BA44001AC618 /* AutoconsentBackgroundTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31055CD27A1BA44001AC618 /* AutoconsentBackgroundTests.swift */; }; + B3FB198E27BC013C00513DC1 /* autoconsent-test-page.html in Resources */ = {isa = PBXBuildFile; fileRef = B3FB198D27BC013C00513DC1 /* autoconsent-test-page.html */; }; + B3FB199027BC015600513DC1 /* autoconsent-test.js in Resources */ = {isa = PBXBuildFile; fileRef = B3FB198F27BC015600513DC1 /* autoconsent-test.js */; }; + B3FB199327BD0AD400513DC1 /* CookieConsentInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FB199227BD0AD400513DC1 /* CookieConsentInfo.swift */; }; B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6040855274B830F00680351 /* DictionaryExtension.swift */; }; B604085C274B8FBA00680351 /* UnprotectedDomains.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B604085A274B8CA300680351 /* UnprotectedDomains.xcdatamodeld */; }; B6085D062743905F00A9C456 /* CoreDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6085D052743905F00A9C456 /* CoreDataStore.swift */; }; @@ -1146,6 +1158,18 @@ AAF7D3852567CED500998667 /* WebViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewConfiguration.swift; sourceTree = ""; }; AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherExtension.swift; sourceTree = ""; }; AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProvider.swift; sourceTree = ""; }; + B31055BC27A1BA1D001AC618 /* AutoconsentUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AutoconsentUserScript.swift; path = Autoconsent/AutoconsentUserScript.swift; sourceTree = ""; }; + B31055BD27A1BA1D001AC618 /* autoconsent.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = autoconsent.html; path = Autoconsent/autoconsent.html; sourceTree = ""; }; + B31055BE27A1BA1D001AC618 /* userscript.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = userscript.js; path = Autoconsent/userscript.js; sourceTree = ""; }; + B31055BF27A1BA1D001AC618 /* browser-shim.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "browser-shim.js"; path = "Autoconsent/browser-shim.js"; sourceTree = ""; }; + B31055C027A1BA1D001AC618 /* background-bundle.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "background-bundle.js"; path = "Autoconsent/background-bundle.js"; sourceTree = ""; }; + B31055C127A1BA1D001AC618 /* AutoconsentBackground.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AutoconsentBackground.swift; path = Autoconsent/AutoconsentBackground.swift; sourceTree = ""; }; + B31055C227A1BA1D001AC618 /* background.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = background.js; path = Autoconsent/background.js; sourceTree = ""; }; + B31055C327A1BA1D001AC618 /* autoconsent-bundle.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "autoconsent-bundle.js"; path = "Autoconsent/autoconsent-bundle.js"; sourceTree = ""; }; + B31055CD27A1BA44001AC618 /* AutoconsentBackgroundTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AutoconsentBackgroundTests.swift; path = Autoconsent/AutoconsentBackgroundTests.swift; sourceTree = ""; }; + B3FB198D27BC013C00513DC1 /* autoconsent-test-page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "autoconsent-test-page.html"; sourceTree = ""; }; + B3FB198F27BC015600513DC1 /* autoconsent-test.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "autoconsent-test.js"; sourceTree = ""; }; + B3FB199227BD0AD400513DC1 /* CookieConsentInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentInfo.swift; sourceTree = ""; }; B6040855274B830F00680351 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = ""; }; B604085B274B8CA400680351 /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = ""; }; B6085D052743905F00A9C456 /* CoreDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStore.swift; sourceTree = ""; }; @@ -1531,6 +1555,7 @@ 4B1AD89E25FC27E200261379 /* Integration Tests */ = { isa = PBXGroup; children = ( + B31055CC27A1BA39001AC618 /* Autoconsent */, 4B1AD91625FC46FB00261379 /* CoreDataEncryptionTests.swift */, 4BA1A6EA258C288C00F6F690 /* EncryptionKeyStoreTests.swift */, 4B1AD8A125FC27E200261379 /* Info.plist */, @@ -2341,6 +2366,7 @@ AA585D80248FD31100E9A3E2 /* DuckDuckGo */ = { isa = PBXGroup; children = ( + B31055BB27A1BA0E001AC618 /* Autoconsent */, B6A9E47526146A440067D1B9 /* API */, AA4D700525545EDE00C3411E /* AppDelegate */, AAC5E4C025D6A6A9007F5990 /* Bookmarks */, @@ -3226,6 +3252,31 @@ path = View; sourceTree = ""; }; + B31055BB27A1BA0E001AC618 /* Autoconsent */ = { + isa = PBXGroup; + children = ( + B31055C327A1BA1D001AC618 /* autoconsent-bundle.js */, + B31055BD27A1BA1D001AC618 /* autoconsent.html */, + B31055C127A1BA1D001AC618 /* AutoconsentBackground.swift */, + B31055BC27A1BA1D001AC618 /* AutoconsentUserScript.swift */, + B31055C027A1BA1D001AC618 /* background-bundle.js */, + B31055C227A1BA1D001AC618 /* background.js */, + B31055BF27A1BA1D001AC618 /* browser-shim.js */, + B31055BE27A1BA1D001AC618 /* userscript.js */, + ); + name = Autoconsent; + sourceTree = ""; + }; + B31055CC27A1BA39001AC618 /* Autoconsent */ = { + isa = PBXGroup; + children = ( + B31055CD27A1BA44001AC618 /* AutoconsentBackgroundTests.swift */, + B3FB198D27BC013C00513DC1 /* autoconsent-test-page.html */, + B3FB198F27BC015600513DC1 /* autoconsent-test.js */, + ); + name = Autoconsent; + sourceTree = ""; + }; B6040859274B8C5200680351 /* Unprotected Domains */ = { isa = PBXGroup; children = ( @@ -3322,6 +3373,7 @@ B6106BA226A7BEA00013B453 /* PermissionAuthorizationState.swift */, AA9B7C7D26A06E040008D425 /* TrackerInfo.swift */, AA9B7C8226A197A00008D425 /* ServerTrust.swift */, + B3FB199227BD0AD400513DC1 /* CookieConsentInfo.swift */, ); path = Model; sourceTree = ""; @@ -3659,6 +3711,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B3FB199027BC015600513DC1 /* autoconsent-test.js in Resources */, + B3FB198E27BC013C00513DC1 /* autoconsent-test-page.html in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3678,14 +3732,18 @@ AA693E5E2696E5B90007BB78 /* CrashReports.storyboard in Resources */, 9833913127AAA4B500DAF119 /* trackerData.json in Resources */, 4B0511CE262CAA5A00F6079C /* DownloadPreferencesTableCellView.xib in Resources */, + B31055CA27A1BA1D001AC618 /* background.js in Resources */, 8511E18425F82B34002F516B /* 01_Fire_really_small.json in Resources */, 85B7184A27677C2D00B4277F /* Onboarding.storyboard in Resources */, 4B0511C3262CAA5A00F6079C /* Preferences.storyboard in Resources */, EA477680272A21B700419EDA /* clickToLoadConfig.json in Resources */, B6B1E88226D5DAC30062C350 /* Downloads.storyboard in Resources */, AA3439712754D4E900B241FA /* dark-shield.json in Resources */, + B31055C827A1BA1D001AC618 /* background-bundle.js in Resources */, + B31055CB27A1BA1D001AC618 /* autoconsent-bundle.js in Resources */, 85A0117425AF2EDF00FA6A0C /* FindInPage.storyboard in Resources */, AA80EC89256C49B8007083E7 /* Localizable.strings in Resources */, + B31055C627A1BA1D001AC618 /* userscript.js in Resources */, EA4617F0273A28A700F110A2 /* fb-tds.json in Resources */, AAE8B102258A41C000E81239 /* Tooltip.storyboard in Resources */, AA68C3D72490F821001B8783 /* README.md in Resources */, @@ -3706,9 +3764,11 @@ AAE71E3825F7869300D74437 /* HomepageCollectionViewItem.xib in Resources */, AA3439792754D55100B241FA /* trackers-1.json in Resources */, AA34397C2754D55100B241FA /* dark-trackers-1.json in Resources */, + B31055C527A1BA1D001AC618 /* autoconsent.html in Resources */, 4B723E1126B0006C00E14D75 /* DataImport.storyboard in Resources */, 4B92929026670D1700AD2C21 /* BookmarkTableCellView.xib in Resources */, 339A6B5826A044BA00E3DAE8 /* duckduckgo-privacy-dashboard in Resources */, + B31055C727A1BA1D001AC618 /* browser-shim.js in Resources */, 4B92928E26670D1700AD2C21 /* BookmarkOutlineViewCell.xib in Resources */, 858C78FC2705EB5F009B2B44 /* HomepageHeader.xib in Resources */, B64C84DE2692D7400048FEBE /* PermissionAuthorization.storyboard in Resources */, @@ -3828,6 +3888,7 @@ files = ( B662D3DF275616FF0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */, 4B1AD8E225FC390B00261379 /* EncryptionMocks.swift in Sources */, + B31055CE27A1BA44001AC618 /* AutoconsentBackgroundTests.swift in Sources */, 4B1AD91725FC46FB00261379 /* CoreDataEncryptionTests.swift in Sources */, 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */, 4B1AD92125FC474E00261379 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, @@ -4096,6 +4157,8 @@ 4B8D9062276D1D880078DB17 /* LocaleExtension.swift in Sources */, AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */, B63D467A25BFC3E100874977 /* NSCoderExtensions.swift in Sources */, + B31055C927A1BA1D001AC618 /* AutoconsentBackground.swift in Sources */, + B3FB199327BD0AD400513DC1 /* CookieConsentInfo.swift in Sources */, B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */, B68458C525C7EA0C00DC17B6 /* TabCollection+NSSecureCoding.swift in Sources */, @@ -4255,6 +4318,7 @@ B6CF78DE267B099C00CD4F13 /* WKNavigationActionExtension.swift in Sources */, AA7412B224D0B3AC00D22FE0 /* TabBarViewItem.swift in Sources */, 856C98D52570116900A22F1F /* NSWindow+Toast.swift in Sources */, + B31055C427A1BA1D001AC618 /* AutoconsentUserScript.swift in Sources */, 859E7D6B27453BF3009C2B69 /* BookmarksExporter.swift in Sources */, 4B5FF67826B602B100D42879 /* FirefoxDataImporter.swift in Sources */, 4B02198B25E05FAC00ED7DEA /* FireproofInfoViewController.swift in Sources */, diff --git a/DuckDuckGo/Autoconsent/AutoconsentBackground.swift b/DuckDuckGo/Autoconsent/AutoconsentBackground.swift new file mode 100644 index 0000000000..9745533cc1 --- /dev/null +++ b/DuckDuckGo/Autoconsent/AutoconsentBackground.swift @@ -0,0 +1,290 @@ +// +// AutoconsentBackground.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import WebKit +import os +import BrowserServicesKit + +protocol AutoconsentManagement { + func clearCache() +} + +/// Central controller of autoconsent rules. Used by AutoconsentUserScript to query autoconsent rules +/// and coordinate their execution on tabs. +@available(macOS 11, *) +final class AutoconsentBackground: NSObject, WKScriptMessageHandlerWithReply, AutoconsentManagement { + + let tabMessageName = "browserTabsMessage" + let actionCallbackName = "actionResponse" + let readyMessageName = "ready" + + var injectionTime: WKUserScriptInjectionTime { .atDocumentStart } + var forMainFrameOnly: Bool { true } + let source: String = { + AutoconsentUserScript.loadJS("browser-shim", from: .main) + }() + + var tabs = [Int: TabFrameTracker]() + var messageCounter = 1 + var actionCallbacks = [Int: (Result) -> Void]() + private var ready = false + private var readyCallbacks: [() async -> Void] = [] + + let background: WKWebView + let decoder = JSONDecoder() + + var sitesNotifiedCache = Set() + + override init() { + let configuration = WKWebViewConfiguration() + background = WKWebView(frame: .zero, configuration: configuration) + super.init() + // configure background webview for two-way messaging. + configuration.userContentController.addUserScript(WKUserScript(source: source, + injectionTime: injectionTime, forMainFrameOnly: true, in: .page)) + configuration.userContentController.addScriptMessageHandler(self, contentWorld: .page, name: tabMessageName) + configuration.userContentController.addScriptMessageHandler(self, contentWorld: .page, name: actionCallbackName) + configuration.userContentController.addScriptMessageHandler(self, contentWorld: .page, name: readyMessageName) + let url = Bundle.main.url(forResource: "autoconsent", withExtension: "html")! + background.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) + } + + func ready(onReady: @escaping () async -> Void) { + if ready { + DispatchQueue.main.async { + Task { await onReady() } + } + + } else { + readyCallbacks.append(onReady) + } + } + + /// Runs an action on the autoconsent background page. This action can be one of: + /// - `detectCMP`: Check if there is a known CMP (Consent Management Platform) present on the page. + /// - `detectPopup`: If there is a CMP, check if they are showing the user a popup. + /// - `doOptOut`: Execute a series of clicks in the page to dismiss the popup and opt the user out of all configurable options. + /// - `selfTest`: If implemented for thie CMP, read back the consent state to check that the opt out was successful. + /// + /// The result of the action is provided in an async callback. + func callAction(in tabId: Int, action: Action, resultCallback: @escaping (Result) -> Void) { + // create a unique message ID so we can retrieve the callback when a response comes from the background page + let callbackId = messageCounter + messageCounter += 1 + self.actionCallbacks[callbackId] = resultCallback + background.evaluateJavaScript("window.callAction(\(callbackId), \(tabId), '\(action)')", in: nil, in: .page, completionHandler: { (result) in + switch result { + case .success: + break + case .failure(let error): + self.actionCallbacks[callbackId] = nil + resultCallback(.failure(error)) + } + }) + } + + /// Async version of callAction + @MainActor func callActionAsync(in tabId: Int, action: Action) async throws -> ActionResponse { + return try await withCheckedThrowingContinuation { continuation in + self.callAction(in: tabId, action: action, resultCallback: {result in + continuation.resume(with: result) + }) + } + } + + func detectCmp(in tabId: Int) async -> ActionResponse? { + do { + return try await callActionAsync(in: tabId, action: .detectCMP) + } catch { + return nil + } + } + + func isPopupOpen(in tabId: Int) async -> Bool { + do { + let response = try await callActionAsync(in: tabId, action: .detectPopup) + return response.result + } catch { + return false + } + } + + func doOptOut(in tabId: Int) async -> Bool { + do { + let response = try await callActionAsync(in: tabId, action: .doOptOut) + return response.result + } catch { + return false + } + } + + func testOptOutWorked(in tabId: Int) async throws -> ActionResponse { + return try await callActionAsync(in: tabId, action: .doOptOut) + } + + /// Process a message sent from the autoconsent userscript. + func onUserScriptMessage(in tabId: Int, _ message: WKScriptMessage) { + let webview = message.webView + let frame = message.frameInfo + var frameId = frame.hashValue + let ref = tabs[tabId] ?? TabFrameTracker() + + if frame.isMainFrame { + frameId = 0 + } + + ref.webview = webview + ref.frames[frameId] = frame + + // check for tabs which have been gced (i.e. the weak reference is now nil). These can be cleaned up both here and in the background page. + for (id, tab) in tabs where tab.webview == nil { + tabs[id] = nil + // delete entry in background script + background.evaluateJavaScript("window.autoconsent.removeTab(\(id));") + } + tabs[tabId] = ref + + let script = "_nativeMessageHandler(\(tabId), \(frameId), \(message.body));" + return background.evaluateJavaScript(script) + } + + func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void) { + if message.name == tabMessageName { + // This is a message sent from the background to a specific tab and frame. We have to find the correct WKWebview and FrameInfo + // instances in order to push the message to the Userscript. + guard let jsonMessage = message.body as? String else { + replyHandler(false, "data decoding error") + return + } + forwardMessageToTab(message: jsonMessage, replyHandler: replyHandler) + } else if message.name == actionCallbackName { + // This is a message response to a call to #callAction. + guard let jsonMessage = message.body as? String, + let response = try? decoder.decode(ActionResponse.self, from: Data(jsonMessage.utf8)), + let callback = actionCallbacks[response.messageId] else { + replyHandler(nil, "Failed to parse message") + return + } + actionCallbacks[response.messageId] = nil + if response.error != nil { + os_log("Action error: %s", log: .autoconsent, type: .error, String(describing: response.error)) + callback(.failure(BackgroundError.actionError)) + } else { + callback(.success(response)) + } + replyHandler("OK", nil) + } else if message.name == readyMessageName { + ready = true + DispatchQueue.main.async { + self.readyCallbacks.forEach({ cb in Task { await cb() } }) + self.readyCallbacks.removeAll() + } + replyHandler("OK", nil) + } + } + + func forwardMessageToTab(message jsonMessage: String, replyHandler: @escaping (Any?, String?) -> Void) { + guard let payload = try? decoder.decode(BrowserTabMessage.self, from: Data(jsonMessage.utf8)) else { + replyHandler(false, "data decoding error") + return + } + let ref = tabs[payload.tabId] + guard let webview = ref?.webview, let frame = ref?.frames[payload.frameId] else { + replyHandler(false, "missing frame target") + return + } + var world: WKContentWorld = .defaultClient + var script = "window.autoconsent(\(jsonMessage))" + // Special case: for eval just run the script in page scope. + if payload.message.type == "eval" { + world = .page + script = """ +(() => { +try { + return !!(\(payload.message.script ?? "{}")) +} catch (e) {} +})(); +""" + } + + webview.evaluateJavaScript(script, in: frame, in: world, completionHandler: { (result) in + switch result { + case.failure(let error): + replyHandler(nil, "Error running \"\(script)\": \(error)") + case.success(let value): + replyHandler(value, nil) + } + }) + } + + func updateSettings(settings: [String: Any]?) { + let encoder = JSONEncoder() + guard let disabledCMPs = settings?["disabledCMPs"] as? [String], + let data = try? encoder.encode(disabledCMPs), + let cmpList = String(data: data, encoding: .utf8) else { + return + } + background.evaluateJavaScript("window.autoconsent.disableCMPs(\(cmpList));") + } + + func clearCache() { + sitesNotifiedCache.removeAll() + } + + final class TabFrameTracker { + weak var webview: WKWebView? + var frames = [Int: WKFrameInfo]() + } + + struct BrowserTabMessage: Codable { + var messageId: Int + var tabId: Int + var frameId: Int + var message: ContentScriptMessage + } + + struct ContentScriptMessage: Codable { + var type: String + var script: String? + var selectors: [String]? + } + + struct ActionResponse: Codable { + var messageId: Int + var ruleName: String? + var result: Bool + var error: String? + } + + enum BackgroundError: Error { + case invalidResponse + case actionError + } + + enum Action { + case detectCMP + case detectPopup + case doOptOut + case selfTest + case prehide + case unhide + } + +} diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift new file mode 100644 index 0000000000..dfea45f5db --- /dev/null +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -0,0 +1,199 @@ +// +// AutoconsentUserScript.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import WebKit +import os +import BrowserServicesKit + +protocol AutoconsentUserScriptDelegate: AnyObject { + func autoconsentUserScript(consentStatus: CookieConsentInfo) +} + +protocol UserScriptWithAutoconsent: UserScript { + var delegate: AutoconsentUserScriptDelegate? { get set } +} + +@available(macOS 11, *) +final class AutoconsentUserScript: NSObject, UserScriptWithAutoconsent { + + static var globalTabCounter = 0 + static var promptLastShown: Date? + static let background = AutoconsentBackground() + + var injectionTime: WKUserScriptInjectionTime { .atDocumentStart } + var forMainFrameOnly: Bool { false } + + enum Constants { + static let newSitePopupHidden = Notification.Name("newSitePopupHidden") + static let popupHiddenHostKey = "popupHiddenHostKey" + } + + private enum MessageName: String, CaseIterable { + case autoconsentBackgroundMessage + case autoconsentPageReady + } + public var messageNames: [String] { MessageName.allCases.map(\.rawValue) } + let source: String + let tabId: Int + let config: PrivacyConfiguration + var actionInProgress = false + var webview: WKWebView? + weak var delegate: AutoconsentUserScriptDelegate? + + init(scriptSource: ScriptSourceProviding = DefaultScriptSourceProvider.shared, + config: PrivacyConfiguration = ContentBlocking.privacyConfigurationManager.privacyConfig) { + source = Self.loadJS("autoconsent-bundle", from: .main, withReplacements: [:]) + Self.globalTabCounter += 1 + tabId = Self.globalTabCounter + self.config = config + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let messageName = MessageName(rawValue: message.name) else { return } + if message.webView != nil { + webview = message.webView! + } + + switch messageName { + case .autoconsentBackgroundMessage: + // forward messages from Userscript to the background + return Self.background.onUserScriptMessage(in: tabId, message) + case .autoconsentPageReady: + // Page ready event (main frame): trigger CMP detection and opt-out if popup is being shown. + os_log("page ready: %s", log: .autoconsent, type: .debug, String(describing: message.body)) + guard let url = URL(string: message.body as? String ?? "") else { + return + } + onPageReady(url: url) + } + } + + func onPageReady(url: URL) { + let preferences = PrivacySecurityPreferences() + + guard preferences.autoconsentEnabled != false else { + os_log("autoconsent is disabled", log: .autoconsent, type: .debug) + return + } + + // reset dashboard state + self.delegate?.autoconsentUserScript(consentStatus: CookieConsentInfo( + consentManaged: Self.background.sitesNotifiedCache.contains(url.host ?? ""), optoutFailed: nil, selftestFailed: nil)) + + guard config.isFeature(.autoconsent, enabledForDomain: url.host) else { + os_log("disabled for site: %s", log: .autoconsent, type: .info, String(describing: url.absoluteString)) + return + } + + guard actionInProgress == false else { + return + } + + self.actionInProgress = true + + Self.background.ready { + // push current privacy config settings to the background page + Self.background.updateSettings(settings: self.config.settings(for: .autoconsent)) + let cmp = await Self.background.detectCmp(in: self.tabId) + guard cmp?.result == true else { + os_log("no CMP detected", log: .autoconsent, type: .info) + self.actionInProgress = false + return + } + os_log("popup found from %s", log: .autoconsent, type: .info, String(describing: cmp?.ruleName)) + // check if the user has explicitly enabled the feature + self.checkUserWasPrompted { enabled in + guard enabled else { + self.actionInProgress = false + return + } + Task { + await self.runOptOut(for: cmp!, on: url) + } + } + } + } + + func checkUserWasPrompted(callback: @escaping (Bool) -> Void) { + var preferences = PrivacySecurityPreferences() + guard preferences.autoconsentEnabled == nil else { + callback(true) + return + } + let now = Date.init() + guard Self.promptLastShown == nil || now > Self.promptLastShown!.addingTimeInterval(30) else { + callback(false) + return + } + Self.promptLastShown = now + let alert = NSAlert.cookiePopup() + alert.beginSheetModal(for: self.webview!.window!, completionHandler: { response in + switch response { + case .alertFirstButtonReturn: + // User wants to turn on the feature + preferences.autoconsentEnabled = true + callback(true) + case .alertSecondButtonReturn: + // "Not now" + callback(false) + case .alertThirdButtonReturn: + // "Don't ask again" + preferences.autoconsentEnabled = false + callback(false) + case _: + callback(false) + } + }) + } + + func runOptOut(for cmp: AutoconsentBackground.ActionResponse, on url: URL) async { + guard await Self.background.isPopupOpen(in: self.tabId) else { + os_log("popup not open", log: .autoconsent, type: .debug) + self.actionInProgress = false + return + } + + let optOutSuccessful = await Self.background.doOptOut(in: self.tabId) + guard optOutSuccessful else { + os_log("opt out failed: %s", log: .autoconsent, type: .error, String(describing: cmp.ruleName)) + self.delegate?.autoconsentUserScript(consentStatus: CookieConsentInfo( + consentManaged: true, optoutFailed: true, selftestFailed: nil)) + self.actionInProgress = false + return + } + os_log("opted out: %s", log: .autoconsent, type: .info, String(describing: cmp.ruleName)) + // post popover notification on main thread + DispatchQueue.main.async { + NotificationCenter.default.post(name: Constants.newSitePopupHidden, object: self, userInfo: [ + Constants.popupHiddenHostKey: url.host ?? "" + ]) + } + + do { + let response = try await Self.background.testOptOutWorked(in: self.tabId) + os_log("self test successful?: %s", log: .autoconsent, type: .debug, String(describing: response.result)) + self.delegate?.autoconsentUserScript(consentStatus: CookieConsentInfo( + consentManaged: true, optoutFailed: false, selftestFailed: false)) + } catch { + os_log("self test error: %s", log: .autoconsent, type: .error, error.localizedDescription) + self.delegate?.autoconsentUserScript(consentStatus: CookieConsentInfo( + consentManaged: true, optoutFailed: false, selftestFailed: true)) + } + self.actionInProgress = false + } +} diff --git a/DuckDuckGo/Autoconsent/autoconsent-bundle.js b/DuckDuckGo/Autoconsent/autoconsent-bundle.js new file mode 100644 index 0000000000..23a96f7ebb --- /dev/null +++ b/DuckDuckGo/Autoconsent/autoconsent-bundle.js @@ -0,0 +1,487 @@ +(function () { + 'use strict'; + + /** + * This code is in most parts copied from https://github.com/cavi-au/Consent-O-Matic/blob/master/Extension/Tools.js + * which is licened under the MIT. + */ + class Tools { + static setBase(base) { + Tools.base = base; + } + static findElement(options, parent = null, multiple = false) { + let possibleTargets = null; + if (parent != null) { + possibleTargets = Array.from(parent.querySelectorAll(options.selector)); + } + else { + if (Tools.base != null) { + possibleTargets = Array.from(Tools.base.querySelectorAll(options.selector)); + } + else { + possibleTargets = Array.from(document.querySelectorAll(options.selector)); + } + } + if (options.textFilter != null) { + possibleTargets = possibleTargets.filter(possibleTarget => { + let textContent = possibleTarget.textContent.toLowerCase(); + if (Array.isArray(options.textFilter)) { + let foundText = false; + for (let text of options.textFilter) { + if (textContent.indexOf(text.toLowerCase()) !== -1) { + foundText = true; + break; + } + } + return foundText; + } + else if (options.textFilter != null) { + return textContent.indexOf(options.textFilter.toLowerCase()) !== -1; + } + }); + } + if (options.styleFilters != null) { + possibleTargets = possibleTargets.filter(possibleTarget => { + let styles = window.getComputedStyle(possibleTarget); + let keep = true; + for (let styleFilter of options.styleFilters) { + let option = styles[styleFilter.option]; + if (styleFilter.negated) { + keep = keep && option !== styleFilter.value; + } + else { + keep = keep && option === styleFilter.value; + } + } + return keep; + }); + } + if (options.displayFilter != null) { + possibleTargets = possibleTargets.filter(possibleTarget => { + if (options.displayFilter) { + //We should be displayed + return possibleTarget.offsetHeight !== 0; + } + else { + //We should not be displayed + return possibleTarget.offsetHeight === 0; + } + }); + } + if (options.iframeFilter != null) { + possibleTargets = possibleTargets.filter(possibleTarget => { + if (options.iframeFilter) { + //We should be inside an iframe + return window.location !== window.parent.location; + } + else { + //We should not be inside an iframe + return window.location === window.parent.location; + } + }); + } + if (options.childFilter != null) { + possibleTargets = possibleTargets.filter(possibleTarget => { + let oldBase = Tools.base; + Tools.setBase(possibleTarget); + let childResults = Tools.find(options.childFilter); + Tools.setBase(oldBase); + return childResults.target != null; + }); + } + if (multiple) { + return possibleTargets; + } + else { + if (possibleTargets.length > 1) { + console.warn("Multiple possible targets: ", possibleTargets, options, parent); + } + return possibleTargets[0]; + } + } + static find(options, multiple = false) { + let results = []; + if (options.parent != null) { + let parent = Tools.findElement(options.parent, null, multiple); + if (parent != null) { + if (parent instanceof Array) { + parent.forEach(p => { + let targets = Tools.findElement(options.target, p, multiple); + if (targets instanceof Array) { + targets.forEach(target => { + results.push({ + parent: p, + target: target + }); + }); + } + else { + results.push({ + parent: p, + target: targets + }); + } + }); + return results; + } + else { + let targets = Tools.findElement(options.target, parent, multiple); + if (targets instanceof Array) { + targets.forEach(target => { + results.push({ + parent: parent, + target: target + }); + }); + } + else { + results.push({ + parent: parent, + target: targets + }); + } + } + } + } + else { + let targets = Tools.findElement(options.target, null, multiple); + if (targets instanceof Array) { + targets.forEach(target => { + results.push({ + parent: null, + target: target + }); + }); + } + else { + results.push({ + parent: null, + target: targets + }); + } + } + if (results.length === 0) { + results.push({ + parent: null, + target: null + }); + } + if (multiple) { + return results; + } + else { + if (results.length !== 1) { + console.warn("Multiple results found, even though multiple false", results); + } + return results[0]; + } + } + } + Tools.base = null; + + function matches(config) { + const result = Tools.find(config); + if (config.type === "css") { + return !!result.target; + } + else if (config.type === "checkbox") { + return !!result.target && result.target.checked; + } + } + async function executeAction(config, param) { + switch (config.type) { + case "click": + return clickAction(config); + case "list": + return listAction(config, param); + case "consent": + return consentAction(config, param); + case "ifcss": + return ifCssAction(config, param); + case "waitcss": + return waitCssAction(config); + case "foreach": + return forEachAction(config, param); + case "hide": + return hideAction(config); + case "slide": + return slideAction(config); + case "close": + return closeAction(); + case "wait": + return waitAction(config); + case "eval": + return evalAction(config); + default: + throw "Unknown action type: " + config.type; + } + } + const STEP_TIMEOUT = 0; + function waitTimeout(timeout) { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, timeout); + }); + } + async function clickAction(config) { + const result = Tools.find(config); + if (result.target != null) { + result.target.click(); + } + return waitTimeout(STEP_TIMEOUT); + } + async function listAction(config, param) { + for (let action of config.actions) { + await executeAction(action, param); + } + } + async function consentAction(config, consentTypes) { + for (const consentConfig of config.consents) { + const shouldEnable = consentTypes.indexOf(consentConfig.type) !== -1; + if (consentConfig.matcher && consentConfig.toggleAction) { + const isEnabled = matches(consentConfig.matcher); + if (isEnabled !== shouldEnable) { + await executeAction(consentConfig.toggleAction); + } + } + else { + if (shouldEnable) { + await executeAction(consentConfig.trueAction); + } + else { + await executeAction(consentConfig.falseAction); + } + } + } + } + async function ifCssAction(config, param) { + const result = Tools.find(config); + if (!result.target) { + if (config.trueAction) { + await executeAction(config.trueAction, param); + } + } + else { + if (config.falseAction) { + await executeAction(config.falseAction, param); + } + } + } + async function waitCssAction(config) { + await new Promise(resolve => { + let numRetries = config.retries || 10; + const waitTime = config.waitTime || 250; + const checkCss = () => { + const result = Tools.find(config); + if ((config.negated && result.target) || + (!config.negated && !result.target)) { + if (numRetries > 0) { + numRetries -= 1; + setTimeout(checkCss, waitTime); + } + else { + resolve(); + } + } + else { + resolve(); + } + }; + checkCss(); + }); + } + async function forEachAction(config, param) { + const results = Tools.find(config, true); + const oldBase = Tools.base; + for (const result of results) { + if (result.target) { + Tools.setBase(result.target); + await executeAction(config.action, param); + } + } + Tools.setBase(oldBase); + } + async function hideAction(config) { + const result = Tools.find(config); + if (result.target) { + result.target.classList.add("Autoconsent-Hidden"); + // result.target.setAttribute("style", "display: none;"); + } + } + async function slideAction(config) { + const result = Tools.find(config); + const dragResult = Tools.find(config.dragTarget); + if (result.target) { + let targetBounds = result.target.getBoundingClientRect(); + let dragTargetBounds = dragResult.target.getBoundingClientRect(); + let yDiff = dragTargetBounds.top - targetBounds.top; + let xDiff = dragTargetBounds.left - targetBounds.left; + if (this.config.axis.toLowerCase() === "y") { + xDiff = 0; + } + if (this.config.axis.toLowerCase() === "x") { + yDiff = 0; + } + let screenX = window.screenX + targetBounds.left + targetBounds.width / 2.0; + let screenY = window.screenY + targetBounds.top + targetBounds.height / 2.0; + let clientX = targetBounds.left + targetBounds.width / 2.0; + let clientY = targetBounds.top + targetBounds.height / 2.0; + let mouseDown = document.createEvent("MouseEvents"); + mouseDown.initMouseEvent("mousedown", true, true, window, 0, screenX, screenY, clientX, clientY, false, false, false, false, 0, result.target); + let mouseMove = document.createEvent("MouseEvents"); + mouseMove.initMouseEvent("mousemove", true, true, window, 0, screenX + xDiff, screenY + yDiff, clientX + xDiff, clientY + yDiff, false, false, false, false, 0, result.target); + let mouseUp = document.createEvent("MouseEvents"); + mouseUp.initMouseEvent("mouseup", true, true, window, 0, screenX + xDiff, screenY + yDiff, clientX + xDiff, clientY + yDiff, false, false, false, false, 0, result.target); + result.target.dispatchEvent(mouseDown); + await this.waitTimeout(10); + result.target.dispatchEvent(mouseMove); + await this.waitTimeout(10); + result.target.dispatchEvent(mouseUp); + } + } + async function waitAction(config) { + await waitTimeout(config.waitTime); + } + async function closeAction(config) { + window.close(); + } + async function evalAction(config) { + console.log("eval!", config.code); + return new Promise(resolve => { + try { + if (config.async) { + window.eval(config.code); + setTimeout(() => { + resolve(window.eval("window.__consentCheckResult")); + }, config.timeout || 250); + } + else { + resolve(window.eval(config.code)); + } + } + catch (e) { + console.warn("eval error", e, config.code); + resolve(false); + } + }); + } + + let actionQueue = Promise.resolve(null); + const styleOverrideElementId = "autoconsent-css-rules"; + const styleSelector = `style#${styleOverrideElementId}`; + function handleMessage(message, debug = false) { + if (message.type === "click") { + const elem = document.querySelectorAll(message.selector); + debug && console.log("[click]", message.selector, elem); + if (elem.length > 0) { + if (message.all === true) { + elem.forEach(e => e.click()); + } + else { + elem[0].click(); + } + } + return elem.length > 0; + } + else if (message.type === "elemExists") { + const exists = document.querySelector(message.selector) !== null; + debug && console.log("[exists?]", message.selector, exists); + return exists; + } + else if (message.type === "elemVisible") { + const elem = document.querySelectorAll(message.selector); + const results = new Array(elem.length); + elem.forEach((e, i) => { + results[i] = e.offsetParent !== null || window.getComputedStyle(e).display !== "none" || e.style?.display !== "none"; + }); + if (results.length === 0) { + return false; + } + else if (message.check === "any") { + return results.some(r => r); + } + else if (message.check === "none") { + return results.every(r => !r); + } + // all + return results.every(r => r); + } + else if (message.type === "getAttribute") { + const elem = document.querySelector(message.selector); + if (!elem) { + return false; + } + return elem.getAttribute(message.attribute); + } + else if (message.type === "eval") { + // TODO: chrome support + const result = window.eval(message.script); // eslint-disable-line no-eval + debug && console.log("[eval]", message.script, result); + return result; + } + else if (message.type === "hide") { + const parent = document.head || + document.getElementsByTagName("head")[0] || + document.documentElement; + const rule = `${message.selectors.join(",")} { display: none !important; z-index: -1 !important; } `; + const existingElement = document.querySelector(styleSelector); + debug && console.log("[hide]", message.selectors, !!existingElement); + if (existingElement && existingElement instanceof HTMLStyleElement) { + existingElement.innerText += rule; + } + else { + const css = document.createElement("style"); + css.type = "text/css"; + css.id = styleOverrideElementId; + css.appendChild(document.createTextNode(rule)); + parent.appendChild(css); + } + return message.selectors.length > 0; + } + else if (message.type === "undohide") { + const existingElement = document.querySelector(styleSelector); + debug && console.log("[unhide]", !!existingElement); + if (existingElement) { + existingElement.remove(); + } + return !!existingElement; + } + else if (message.type === "matches") { + const matched = matches(message.config); + return matched; + } + else if (message.type === "executeAction") { + actionQueue = actionQueue.then(() => executeAction(message.config, message.param)); + return true; + } + return null; + } + + window.autoconsent = (payload) => { + return handleMessage(payload.message, false) + }; + + window.webkit.messageHandlers.autoconsentBackgroundMessage.postMessage(JSON.stringify({ + type: 'webNavigation.onCommitted', + url: window.location.href + })); + + const isMainDocument = window === window.top; + if (isMainDocument) { + setTimeout(() => { + window.webkit.messageHandlers.autoconsentPageReady.postMessage(window.location.href); + }, 100); + } + + window.onload = () => { + window.webkit.messageHandlers.autoconsentBackgroundMessage.postMessage(JSON.stringify({ + type: 'webNavigation.onCompleted', + url: window.location.href + })); + if (isMainDocument) { + window.webkit.messageHandlers.autoconsentPageReady.postMessage(window.location.href); + } + }; + +})(); diff --git a/DuckDuckGo/Autoconsent/autoconsent.html b/DuckDuckGo/Autoconsent/autoconsent.html new file mode 100644 index 0000000000..eca3bdcc46 --- /dev/null +++ b/DuckDuckGo/Autoconsent/autoconsent.html @@ -0,0 +1,26 @@ + + + + + + Autoconsent background + + + + + diff --git a/DuckDuckGo/Autoconsent/background-bundle.js b/DuckDuckGo/Autoconsent/background-bundle.js new file mode 100644 index 0000000000..9195e6f4a8 --- /dev/null +++ b/DuckDuckGo/Autoconsent/background-bundle.js @@ -0,0 +1,3977 @@ +(function () { + 'use strict'; + + /* eslint-disable no-restricted-syntax,no-await-in-loop,no-underscore-dangle */ + async function waitFor(predicate, maxTimes, interval) { + let result = await predicate(); + if (!result && maxTimes > 0) { + return new Promise((resolve) => { + setTimeout(async () => { + resolve(waitFor(predicate, maxTimes - 1, interval)); + }, interval); + }); + } + return Promise.resolve(result); + } + async function success(action) { + const result = await action; + if (!result) { + throw new Error(`Action failed: ${action}`); + } + return result; + } + class AutoConsentBase { + constructor(name) { + this.hasSelfTest = true; + this.name = name; + } + detectCmp(tab) { + throw new Error('Not Implemented'); + } + async detectPopup(tab) { + return false; + } + detectFrame(tab, frame) { + return false; + } + optOut(tab) { + throw new Error('Not Implemented'); + } + optIn(tab) { + throw new Error('Not Implemented'); + } + openCmp(tab) { + throw new Error('Not Implemented'); + } + async test(tab) { + // try IAB by default + return Promise.resolve(true); + } + } + async function evaluateRule(rule, tab) { + if (rule.frame && !tab.frame) { + await waitFor(() => Promise.resolve(!!tab.frame), 10, 500); + } + const frameId = rule.frame && tab.frame ? tab.frame.id : undefined; + const results = []; + if (rule.exists) { + results.push(tab.elementExists(rule.exists, frameId)); + } + if (rule.visible) { + results.push(tab.elementsAreVisible(rule.visible, rule.check, frameId)); + } + if (rule.eval) { + results.push(new Promise(async (resolve) => { + // catch eval error silently + try { + resolve(await tab.eval(rule.eval, frameId)); + } + catch (e) { + resolve(false); + } + })); + } + if (rule.waitFor) { + results.push(tab.waitForElement(rule.waitFor, rule.timeout || 10000, frameId)); + } + if (rule.click) { + if (rule.all === true) { + results.push(tab.clickElements(rule.click, frameId)); + } + else { + results.push(tab.clickElement(rule.click, frameId)); + } + } + if (rule.waitForThenClick) { + results.push(tab.waitForElement(rule.waitForThenClick, rule.timeout || 10000, frameId) + .then(() => tab.clickElement(rule.waitForThenClick, frameId))); + } + if (rule.wait) { + results.push(tab.wait(rule.wait)); + } + if (rule.goto) { + results.push(tab.goto(rule.goto)); + } + if (rule.hide) { + results.push(tab.hideElements(rule.hide, frameId)); + } + if (rule.undoHide) { + results.push(tab.undoHideElements(frameId)); + } + if (rule.waitForFrame) { + results.push(waitFor(() => !!tab.frame, 40, 500)); + } + // boolean and of results + return (await Promise.all(results)).reduce((a, b) => a && b, true); + } + class AutoConsent$1 extends AutoConsentBase { + constructor(config) { + super(config.name); + this.config = config; + } + get prehideSelectors() { + return this.config.prehideSelectors; + } + get isHidingRule() { + return this.config.isHidingRule; + } + async _runRulesParallel(tab, rules) { + const detections = await Promise.all(rules.map(rule => evaluateRule(rule, tab))); + return detections.every(r => !!r); + } + async _runRulesSequentially(tab, rules) { + for (const rule of rules) { + const result = await evaluateRule(rule, tab); + if (!result && !rule.optional) { + return false; + } + } + return true; + } + async detectCmp(tab) { + if (this.config.detectCmp) { + return this._runRulesParallel(tab, this.config.detectCmp); + } + return false; + } + async detectPopup(tab) { + if (this.config.detectPopup) { + return this._runRulesParallel(tab, this.config.detectPopup); + } + return false; + } + detectFrame(tab, frame) { + if (this.config.frame) { + return frame.url.startsWith(this.config.frame); + } + return false; + } + async optOut(tab) { + if (this.config.optOut) { + return this._runRulesSequentially(tab, this.config.optOut); + } + return false; + } + async optIn(tab) { + if (this.config.optIn) { + return this._runRulesSequentially(tab, this.config.optIn); + } + return false; + } + async openCmp(tab) { + if (this.config.openCmp) { + return this._runRulesSequentially(tab, this.config.openCmp); + } + return false; + } + async test(tab) { + if (this.config.test) { + return this._runRulesSequentially(tab, this.config.test); + } + return super.test(tab); + } + } + + class TabActions { + constructor(tabId, frame, sendContentMessage, browser) { + this.frame = frame; + this.sendContentMessage = sendContentMessage; + this.browser = browser; + this.id = tabId; + } + async elementExists(selector, frameId = 0) { + console.log(`check for ${selector} in tab ${this.id}, frame ${frameId}`); + return this.sendContentMessage(this.id, { + type: "elemExists", + selector + }, { + frameId + }); + } + async clickElement(selector, frameId = 0) { + console.log(`click element ${selector} in tab ${this.id}`); + return this.sendContentMessage(this.id, { + type: "click", + selector + }, { + frameId + }); + } + async clickElements(selector, frameId = 0) { + console.log(`click elements ${selector} in tab ${this.id}`); + return this.sendContentMessage(this.id, { + type: "click", + all: true, + selector + }, { + frameId + }); + } + async elementsAreVisible(selector, check, frameId = 0) { + return this.sendContentMessage(this.id, { + type: "elemVisible", + selector, + check + }, { + frameId + }); + } + async getAttribute(selector, attribute, frameId = 0) { + return this.sendContentMessage(this.id, { + type: "getAttribute", + selector, + attribute + }, { frameId }); + } + async eval(script, frameId = 0) { + // console.log(`run ${script} in tab ${this.id}`); + return await this.sendContentMessage(this.id, { + type: "eval", + script + }, { frameId }); + } + async waitForElement(selector, timeout, frameId = 0) { + const interval = 200; + const times = Math.ceil(timeout / interval); + return waitFor(() => this.elementExists(selector, frameId), times, interval); + } + async waitForThenClick(selector, timeout, frameId = 0) { + if (await this.waitForElement(selector, timeout, frameId)) { + return await this.clickElement(selector, frameId); + } + return false; + } + async hideElements(selectors, frameId = 0) { + return this.sendContentMessage(this.id, { + type: "hide", + selectors + }, { frameId }); + } + async undoHideElements(frameId = 0) { + return this.sendContentMessage(this.id, { + type: "undohide", + }, { frameId }); + } + async getBrowserTab() { + return this.browser.tabs.get(this.id); + } + async goto(url) { + return this.browser.tabs.update(this.id, { url }); + } + wait(ms) { + return new Promise(resolve => { + setTimeout(() => resolve(true), ms); + }); + } + matches(matcherConfig) { + return this.sendContentMessage(this.id, { + type: "matches", + config: matcherConfig + }, { frameId: 0 }); + } + executeAction(config, param) { + return this.sendContentMessage(this.id, { + type: "executeAction", + config, + param + }, { frameId: 0 }); + } + } + + Promise.resolve(null); + + class TabConsent { + constructor(tab, ruleCheckPromise) { + this.tab = tab; + this.optOutStatus = null; + this.checked = ruleCheckPromise; + ruleCheckPromise.then(rule => this.rule = rule); + } + getCMPName() { + if (this.rule) { + return this.rule.name; + } + return null; + } + async isPopupOpen(retries = 1, interval = 1000) { + const isOpen = await this.rule.detectPopup(this.tab); + if (!isOpen && retries > 0) { + return new Promise((resolve) => setTimeout(() => resolve(this.isPopupOpen(retries - 1, interval)), interval)); + } + return isOpen; + } + async doOptOut() { + try { + this.optOutStatus = await this.rule.optOut(this.tab); + return this.optOutStatus; + } + catch (e) { + this.optOutStatus = e; + throw e; + } + finally { + if (!this.rule.isHidingRule) { + if (this.getCMPName().startsWith('com_')) { + this.tab.wait(5000).then(() => this.tab.undoHideElements()); + } + else { + await this.tab.undoHideElements(); + } + } + } + } + async doOptIn() { + try { + return this.rule.optIn(this.tab); + } + finally { + if (!this.rule.isHidingRule) { + await this.tab.undoHideElements(); + } + } + } + hasTest() { + return !!this.rule.hasSelfTest; + } + async testOptOutWorked() { + return this.rule.test(this.tab); + } + async applyCosmetics(selectors) { + const hidden = await this.tab.hideElements(selectors); + return hidden; + } + } + + async function detectDialog(tab, retries, rules) { + let breakEarly = false; + const found = await new Promise(async (resolve) => { + let earlyReturn = false; + await Promise.all(rules.map(async (r, index) => { + try { + if (await r.detectCmp(tab)) { + earlyReturn = true; + resolve(index); + } + } + catch (e) { + breakEarly = true; + } + })); + if (!earlyReturn) { + resolve(-1); + } + }); + if (found === -1 && retries > 0 && !breakEarly) { + return new Promise((resolve) => { + setTimeout(async () => { + const result = detectDialog(tab, retries - 1, rules); + resolve(result); + }, 500); + }); + } + return found > -1 ? rules[found] : null; + } + + class TrustArc extends AutoConsentBase { + constructor() { + super("TrustArc"); + this.prehideSelectors = [ + ".trustarc-banner-container", + ".truste_popframe,.truste_overlay,.truste_box_overlay,#truste-consent-track", + ]; + } + detectFrame(_, frame) { + return frame.url.startsWith("https://consent-pref.trustarc.com/?"); + } + async detectCmp(tab) { + if (tab.frame && + tab.frame.url.startsWith("https://consent-pref.trustarc.com/?")) { + return true; + } + return tab.elementExists("#truste-show-consent"); + } + async detectPopup(tab) { + return ((await tab.elementsAreVisible("#truste-consent-content,#trustarc-banner-overlay")) || + (tab.frame && + (await tab.waitForElement("#defaultpreferencemanager", 5000, tab.frame.id)))); + } + async openFrame(tab) { + if (await tab.elementExists("#truste-show-consent")) { + await tab.clickElement("#truste-show-consent"); + } + } + async navigateToSettings(tab, frameId) { + // wait for it to load + await waitFor(async () => { + return ((await tab.elementExists(".shp", frameId)) || + (await tab.elementsAreVisible(".advance", "any", frameId)) || + tab.elementExists(".switch span:first-child", frameId)); + }, 10, 500); + // splash screen -> hit more information + if (await tab.elementExists(".shp", frameId)) { + await tab.clickElement(".shp", frameId); + } + await tab.waitForElement(".prefPanel", 5000, frameId); + // go to advanced settings if not yet shown + if (await tab.elementsAreVisible(".advance", "any", frameId)) { + await tab.clickElement(".advance", frameId); + } + // takes a while to load the opt-in/opt-out buttons + return await waitFor(() => tab.elementsAreVisible(".switch span:first-child", "any", frameId), 5, 1000); + } + async optOut(tab) { + // await tab.hideElements(['.truste_overlay', '.truste_box_overlay', '.trustarc-banner', '.truste-banner']); + if (await tab.elementExists("#truste-consent-required")) { + return tab.clickElement("#truste-consent-required"); + } + if (!tab.frame) { + await tab.clickElement("#truste-show-consent"); + await waitFor(async () => !!tab.frame && + (await tab.elementsAreVisible(".mainContent", "any", tab.frame.id)), 50, 100); + } + const frameId = tab.frame.id; + await waitFor(() => tab.eval("document.readyState === 'complete'", frameId), 20, 100); + tab.hideElements([".truste_popframe", ".truste_overlay", ".truste_box_overlay", "#truste-consent-track"]); + if (await tab.elementExists('.rejectAll', frameId)) { + return tab.clickElement('.rejectAll', frameId); + } + if (await tab.waitForElement('#catDetails0', 1000, frameId)) { + await tab.clickElement("#catDetails0", frameId); + return tab.clickElement(".submit", frameId); + } + if (await tab.elementExists(".required", frameId)) { + await tab.clickElement(".required", frameId); + } + else { + await this.navigateToSettings(tab, frameId); + await tab.clickElements(".switch span:nth-child(1):not(.active)", frameId); + await tab.clickElement(".submit", frameId); + } + try { + await tab.waitForThenClick("#gwt-debug-close_id", 20000, tab.frame.id); + } + catch (e) { + // ignore frame disappearing + } + return true; + } + async optIn(tab) { + if (!tab.frame) { + await this.openFrame(tab); + await waitFor(() => !!tab.frame, 10, 200); + } + const frameId = tab.frame.id; + await this.navigateToSettings(tab, frameId); + await tab.clickElements(".switch span:nth-child(2)", frameId); + await tab.clickElement(".submit", frameId); + await waitFor(() => tab.elementExists("#gwt-debug-close_id", frameId), 300, 1000); + await tab.clickElement("#gwt-debug-close_id", frameId); + return true; + } + async openCmp(tab) { + await tab.eval("truste.eu.clickListener()"); + return true; + } + async test() { + // TODO: find out how to test TrustArc + return true; + } + } + + class Cookiebot extends AutoConsentBase { + constructor() { + super('Cybotcookiebot'); + this.prehideSelectors = ["#CybotCookiebotDialog,#dtcookie-container,#cookiebanner"]; + } + async detectCmp(tab) { + try { + return await tab.eval('typeof window.CookieConsent === "object" && typeof window.CookieConsent.name === "string"'); + } + catch (e) { + return false; + } + } + detectPopup(tab) { + return tab.elementExists('#CybotCookiebotDialog,#dtcookie-container,#cookiebanner'); + } + async optOut(tab) { + if (await tab.elementExists('.cookie-alert-extended-detail-link')) { + await tab.clickElement('.cookie-alert-extended-detail-link'); + await tab.waitForElement('.cookie-alert-configuration', 1000); + await tab.clickElements('.cookie-alert-configuration-input:checked'); + return tab.clickElement('.cookie-alert-extended-button-secondary'); + } + if (await tab.elementExists('#dtcookie-container')) { + return tab.clickElement('.h-dtcookie-decline'); + } + if (await tab.elementExists('.cookiebot__button--settings')) { + await tab.clickElement('.cookiebot__button--settings'); + } + if (await tab.elementsAreVisible('#CybotCookiebotDialogBodyButtonDecline', 'all')) { + return await tab.clickElement('#CybotCookiebotDialogBodyButtonDecline'); + } + if (await tab.elementExists('.cookiebanner__link--details')) { + await tab.clickElement('.cookiebanner__link--details'); + } + await tab.clickElements('.CybotCookiebotDialogBodyLevelButton:checked:enabled,input[id*="CybotCookiebotDialogBodyLevelButton"]:checked:enabled'); + if (await tab.elementExists('#CybotCookiebotDialogBodyButtonDecline')) { + await tab.clickElement('#CybotCookiebotDialogBodyButtonDecline'); + } + if (await tab.elementExists('input[id^=CybotCookiebotDialogBodyLevelButton]:checked')) { + await tab.clickElements('input[id^=CybotCookiebotDialogBodyLevelButton]:checked'); + } + if (await tab.elementExists('#CybotCookiebotDialogBodyButtonAcceptSelected')) { + await tab.clickElement('#CybotCookiebotDialogBodyButtonAcceptSelected'); + } + else { + await tab.clickElements('#CybotCookiebotDialogBodyLevelButtonAccept,#CybotCookiebotDialogBodyButtonAccept,#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowallSelection'); + } + // some sites have custom submit buttons with no obvious selectors. In this case we just call the submitConsent API. + if (await tab.eval('CookieConsent.hasResponse !== true')) { + await tab.eval('Cookiebot.dialog.submitConsent() || true'); + await tab.wait(500); + } + return true; + } + async optIn(tab) { + if (await tab.elementExists('#dtcookie-container')) { + return tab.clickElement('.h-dtcookie-accept'); + } + await tab.clickElements('.CybotCookiebotDialogBodyLevelButton:not(:checked):enabled'); + await tab.clickElement('#CybotCookiebotDialogBodyLevelButtonAccept'); + await tab.clickElement('#CybotCookiebotDialogBodyButtonAccept'); + return true; + } + async openCmp(tab) { + await tab.eval('CookieConsent.renew() || true'); + return tab.waitForElement('#CybotCookiebotDialog', 10000); + } + async test(tab) { + return tab.eval('CookieConsent.declined === true'); + } + } + + class SourcePoint extends AutoConsentBase { + constructor() { + super("Sourcepoint"); + this.ccpaMode = false; + this.prehideSelectors = ["div[id^='sp_message_container_'],.message-overlay"]; + } + detectFrame(_, frame) { + try { + const url = new URL(frame.url); + if (url.searchParams.has('message_id') && url.hostname === 'ccpa-notice.sp-prod.net') { + this.ccpaMode = true; + return true; + } + return (url.pathname === '/index.html' || url.pathname === '/privacy-manager/index.html') + && url.searchParams.has('message_id') && url.searchParams.has('requestUUID'); + } + catch (e) { + return false; + } + } + async detectCmp(tab) { + return await tab.elementExists("div[id^='sp_message_container_']") || !!tab.frame; + } + async detectPopup(tab) { + return await tab.elementsAreVisible("div[id^='sp_message_container_']"); + } + async optIn(tab) { + return tab.clickElement(".sp_choice_type_11", tab.frame.id); + } + isManagerOpen(tab) { + return tab.frame && new URL(tab.frame.url).pathname === "/privacy-manager/index.html"; + } + async optOut(tab) { + try { + tab.hideElements(["div[id^='sp_message_container_']"]); + if (!this.isManagerOpen(tab)) { + if (!await waitFor(() => !!tab.frame, 30, 100)) { + throw "Frame never opened"; + } + if (!await tab.elementExists("button.sp_choice_type_12", tab.frame.id)) { + // do not sell button + return tab.clickElement('button.sp_choice_type_13', tab.frame.id); + } + await success(tab.clickElement("button.sp_choice_type_12", tab.frame.id)); + await waitFor(() => new URL(tab.frame.url).pathname === "/privacy-manager/index.html", 200, 100); + } + await tab.waitForElement('.type-modal', 20000, tab.frame.id); + // reject all button is offered by some sites + try { + const path = await Promise.race([ + tab.waitForElement('.sp_choice_type_REJECT_ALL', 2000, tab.frame.id).then(r => 0), + tab.waitForElement('.reject-toggle', 2000, tab.frame.id).then(() => 1), + tab.waitForElement('.pm-features', 2000, tab.frame.id).then(r => 2), + ]); + if (path === 0) { + await tab.wait(1000); + return await success(tab.clickElement('.sp_choice_type_REJECT_ALL', tab.frame.id)); + } + else if (path === 1) { + await tab.clickElement('.reject-toggle', tab.frame.id); + } + else { + await tab.waitForElement('.pm-features', 10000, tab.frame.id); + await tab.clickElements('.checked > span', tab.frame.id); + if (await tab.elementExists('.chevron', tab.frame.id)) { + await tab.clickElement('.chevron', tab.frame.id); + } + } + } + catch (e) { } + return await tab.clickElement('.sp_choice_type_SAVE_AND_EXIT', tab.frame.id); + } + finally { + tab.undoHideElements(); + } + } + async test(tab) { + await tab.eval("__tcfapi('getTCData', 2, r => window.__rcsResult = r)"); + return tab.eval("Object.values(window.__rcsResult.purpose.consents).every(c => !c)"); + } + } + + // Note: JS API is also available: + // https://help.consentmanager.net/books/cmp/page/javascript-api + class ConsentManager extends AutoConsentBase { + constructor() { + super("consentmanager.net"); + this.prehideSelectors = ["#cmpbox,#cmpbox2"]; + } + detectCmp(tab) { + return tab.elementExists("#cmpbox"); + } + detectPopup(tab) { + return tab.elementsAreVisible("#cmpbox .cmpmore", "any"); + } + async optOut(tab) { + if (await tab.elementExists(".cmpboxbtnno")) { + return tab.clickElement(".cmpboxbtnno"); + } + if (await tab.elementExists(".cmpwelcomeprpsbtn")) { + await tab.clickElements(".cmpwelcomeprpsbtn > a[aria-checked=true]"); + return await tab.clickElement(".cmpboxbtnsave"); + } + await tab.clickElement(".cmpboxbtncustom"); + await tab.waitForElement(".cmptblbox", 2000); + await tab.clickElements(".cmptdchoice > a[aria-checked=true]"); + return tab.clickElement(".cmpboxbtnyescustomchoices"); + } + async optIn(tab) { + return tab.clickElement(".cmpboxbtnyes"); + } + } + + // Note: JS API is also available: + // https://help.consentmanager.net/books/cmp/page/javascript-api + class Evidon extends AutoConsentBase { + constructor() { + super("Evidon"); + } + detectCmp(tab) { + return tab.elementExists("#_evidon_banner"); + } + detectPopup(tab) { + return tab.elementsAreVisible("#_evidon_banner"); + } + async optOut(tab) { + if (await tab.elementExists("#_evidon-decline-button")) { + return tab.clickElement("#_evidon-decline-button"); + } + tab.hideElements(["#evidon-prefdiag-overlay", "#evidon-prefdiag-background"]); + await tab.clickElement("#_evidon-option-button"); + await tab.waitForElement("#evidon-prefdiag-overlay", 5000); + return tab.clickElement("#evidon-prefdiag-decline"); + } + async optIn(tab) { + return tab.clickElement("#_evidon-accept-button"); + } + } + + const rules$3 = [ + new TrustArc(), + new Cookiebot(), + new SourcePoint(), + new ConsentManager(), + new Evidon(), + ]; + function createAutoCMP(config) { + return new AutoConsent$1(config); + } + + const rules$2 = rules$3; + + class ConsentOMaticCMP { + constructor(name, config) { + this.name = name; + this.config = config; + this.methods = new Map(); + config.methods.forEach(methodConfig => { + if (methodConfig.action) { + this.methods.set(methodConfig.name, methodConfig.action); + } + }); + this.hasSelfTest = this.methods.has("TEST_CONSENT"); + } + async detectCmp(tab) { + return (await Promise.all(this.config.detectors.map(detectorConfig => tab.matches(detectorConfig.presentMatcher)))).some(matched => matched); + } + async detectPopup(tab) { + return (await Promise.all(this.config.detectors.map(detectorConfig => tab.matches(detectorConfig.showingMatcher)))).some(matched => matched); + } + async executeAction(tab, method, param) { + if (this.methods.has(method)) { + return tab.executeAction(this.methods.get(method), param); + } + return true; + } + async optOut(tab) { + await this.executeAction(tab, "HIDE_CMP"); + await this.executeAction(tab, "OPEN_OPTIONS"); + await this.executeAction(tab, "HIDE_CMP"); + await this.executeAction(tab, "DO_CONSENT", []); + await this.executeAction(tab, "SAVE_CONSENT"); + return true; + } + async optIn(tab) { + await this.executeAction(tab, "HIDE_CMP"); + await this.executeAction(tab, "OPEN_OPTIONS"); + await this.executeAction(tab, "HIDE_CMP"); + await this.executeAction(tab, "DO_CONSENT", ['D', 'A', 'B', 'E', 'F', 'X']); + await this.executeAction(tab, "SAVE_CONSENT"); + return true; + } + async openCmp(tab) { + await this.executeAction(tab, "HIDE_CMP"); + await this.executeAction(tab, "OPEN_OPTIONS"); + return true; + } + test(tab) { + return this.executeAction(tab, "TEST_CONSENT"); + } + detectFrame(tab, frame) { + return false; + } + } + + // hide rules not specific to a single CMP rule + const globalHidden = [ + "#didomi-popup,.didomi-popup-container,.didomi-popup-notice,.didomi-consent-popup-preferences,#didomi-notice,.didomi-popup-backdrop,.didomi-screen-medium", + ]; + async function prehideElements(tab, rules) { + const selectors = rules.reduce((selectorList, rule) => { + if (rule.prehideSelectors) { + return [...selectorList, ...rule.prehideSelectors]; + } + return selectorList; + }, globalHidden); + await tab.hideElements(selectors); + } + + class AutoConsent { + constructor(browser, sendContentMessage) { + this.browser = browser; + this.sendContentMessage = sendContentMessage; + this.consentFrames = new Map(); + this.tabCmps = new Map(); + this.sendContentMessage = sendContentMessage; + this.rules = [...rules$2]; + } + addCMP(config) { + this.rules.push(createAutoCMP(config)); + } + disableCMPs(cmpNames) { + this.rules = this.rules.filter((cmp) => !cmpNames.includes(cmp.name)); + } + addConsentomaticCMP(name, config) { + this.rules.push(new ConsentOMaticCMP(`com_${name}`, config)); + } + createTab(tabId) { + return new TabActions(tabId, this.consentFrames.get(tabId), this.sendContentMessage, this.browser); + } + async checkTab(tabId, prehide = true) { + const tab = this.createTab(tabId); + if (prehide) { + this.prehideElements(tab); + } + const consent = new TabConsent(tab, this.detectDialog(tab, 20)); + this.tabCmps.set(tabId, consent); + // check tabs + consent.checked.then((rule) => { + if (this.consentFrames.has(tabId) && rule) { + const frame = this.consentFrames.get(tabId); + if (frame.type === rule.name) { + consent.tab.frame = frame; + } + } + // no CMP detected, undo hiding + if (!rule && prehide) { + tab.undoHideElements(); + } + }); + return this.tabCmps.get(tabId); + } + removeTab(tabId) { + this.tabCmps.delete(tabId); + this.consentFrames.delete(tabId); + } + onFrame({ tabId, url, frameId }) { + // ignore main frames + if (frameId === 0) { + return; + } + try { + const frame = { + id: frameId, + url: url, + }; + const tab = this.createTab(tabId); + const frameMatch = this.rules.findIndex(r => r.detectFrame(tab, frame)); + if (frameMatch > -1) { + this.consentFrames.set(tabId, { + type: this.rules[frameMatch].name, + url, + id: frameId, + }); + if (this.tabCmps.has(tabId)) { + this.tabCmps.get(tabId).tab.frame = this.consentFrames.get(tabId); + } + } + } + catch (e) { + console.error(e); + } + } + async detectDialog(tab, retries) { + return detectDialog(tab, retries, this.rules); + } + async prehideElements(tab) { + return prehideElements(tab, this.rules); + } + } + + var autoconsent = [ + { + name: "asus", + detectCmp: [ + { + exists: "#cookie-policy-info" + } + ], + detectPopup: [ + { + visible: "#cookie-policy-info" + } + ], + optIn: [ + { + click: ".btn-read-ck" + } + ], + optOut: [ + { + click: ".btn-setting" + }, + { + click: ".btn-save" + } + ] + }, + { + name: "cc_banner", + prehideSelectors: [ + ".cc_banner-wrapper" + ], + isHidingRule: true, + detectCmp: [ + { + exists: ".cc_banner-wrapper" + } + ], + detectPopup: [ + { + visible: ".cc_banner" + } + ], + optIn: [ + { + click: ".cc_btn_accept_all" + } + ], + optOut: [ + { + hide: [ + ".cc_banner-wrapper" + ] + } + ] + }, + { + name: "cookie-law-info", + prehideSelectors: [ + "#cookie-law-info-bar" + ], + detectCmp: [ + { + exists: "#cookie-law-info-bar" + } + ], + detectPopup: [ + { + visible: "#cookie-law-info-bar" + } + ], + optIn: [ + { + click: "[data-cli_action=\"accept\"]" + } + ], + optOut: [ + { + hide: [ + "#cookie-law-info-bar" + ] + }, + { + "eval": "CLI.disableAllCookies() || CLI.reject_close() || true" + } + ], + test: [ + { + "eval": "document.cookie.indexOf('cookielawinfo-checkbox-non-necessary=yes') === -1" + } + ] + }, + { + name: "cookie-notice", + prehideSelectors: [ + "#cookie-notice" + ], + isHidingRule: true, + detectCmp: [ + { + exists: "#cookie-notice" + } + ], + detectPopup: [ + { + visible: "#cookie-notice" + } + ], + optIn: [ + { + hide: [ + "#cn-accept-cookie" + ] + } + ], + optOut: [ + { + hide: [ + "#cookie-notice" + ] + } + ] + }, + { + name: "cookieconsent", + prehideSelectors: [ + "[aria-label=\"cookieconsent\"]" + ], + isHidingRule: true, + detectCmp: [ + { + exists: "[aria-label=\"cookieconsent\"]" + } + ], + detectPopup: [ + { + visible: "[aria-label=\"cookieconsent\"]" + } + ], + optIn: [ + { + click: ".cc-dismiss" + } + ], + optOut: [ + { + hide: [ + "[aria-label=\"cookieconsent\"]" + ] + } + ] + }, + { + name: "Drupal", + detectCmp: [ + { + exists: "#drupalorg-crosssite-gdpr" + } + ], + detectPopup: [ + { + visible: "#drupalorg-crosssite-gdpr" + } + ], + optOut: [ + { + click: ".no" + } + ], + optIn: [ + { + click: ".yes" + } + ] + }, + { + name: "eu-cookie-compliance-banner", + isHidingRule: true, + detectCmp: [ + { + exists: ".eu-cookie-compliance-banner-info" + } + ], + detectPopup: [ + { + visible: ".eu-cookie-compliance-banner-info" + } + ], + optIn: [ + { + click: ".agree-button" + } + ], + optOut: [ + { + click: ".decline-button,.eu-cookie-compliance-save-preferences-button", + optional: true + }, + { + hide: [ + ".eu-cookie-compliance-banner-info", + "#sliding-popup" + ] + } + ], + test: [ + { + "eval": "document.cookie.indexOf('cookie-agreed=2') === -1" + } + ] + }, + { + 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: true, + optional: true + }, + { + click: ".fc-confirm-choices", + optional: true + } + ], + optIn: [ + { + click: ".fc-cta-consent" + } + ] + }, + { + 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: "klaro", + detectCmp: [ + { + exists: ".klaro > .cookie-notice" + } + ], + detectPopup: [ + { + visible: ".klaro > .cookie-notice" + } + ], + optIn: [ + { + click: ".cm-btn-success" + } + ], + optOut: [ + { + click: ".cn-decline" + } + ], + test: [ + { + "eval": "Object.values(klaro.getManager().consents).every(c => !c)" + } + ] + }, + { + name: "notice-cookie", + prehideSelectors: [ + ".button--notice" + ], + isHidingRule: true, + detectCmp: [ + { + exists: ".notice--cookie" + } + ], + detectPopup: [ + { + visible: ".notice--cookie" + } + ], + optIn: [ + { + click: ".button--notice" + } + ], + optOut: [ + { + hide: [ + ".notice--cookie" + ] + } + ] + }, + { + name: "Onetrust", + prehideSelectors: [ + "#onetrust-banner-sdk,#onetrust-consent-sdk,.optanon-alert-box-wrapper,.onetrust-pc-dark-filter,.js-consent-banner" + ], + isHidingRule: true, + detectCmp: [ + { + exists: "#onetrust-banner-sdk,.optanon-alert-box-wrapper" + } + ], + detectPopup: [ + { + visible: "#onetrust-banner-sdk,.optanon-alert-box-wrapper" + } + ], + optOut: [ + { + click: "#onetrust-pc-btn-handler,.ot-sdk-show-settings,button.js-cookie-settings" + }, + { + waitFor: "#onetrust-consent-sdk", + timeout: 2000 + }, + { + wait: 1000 + }, + { + click: "#onetrust-consent-sdk input.category-switch-handler:checked,.js-editor-toggle-state:checked", + all: true, + optional: true + }, + { + waitForThenClick: ".save-preference-btn-handler,.js-consent-save", + timeout: 1000 + } + ], + optIn: [ + { + click: "onetrust-accept-btn-handler,js-accept-cookies" + } + ], + test: [ + { + "eval": "window.OnetrustActiveGroups.split(',').filter(s => s.length > 0).length <= 1" + } + ] + }, + { + name: "osano", + prehideSelectors: [ + ".osano-cm-window" + ], + isHidingRule: true, + detectCmp: [ + { + exists: ".osano-cm-window" + } + ], + detectPopup: [ + { + visible: ".osano-cm-dialog" + } + ], + optIn: [ + { + click: ".osano-cm-accept-all" + } + ], + optOut: [ + { + hide: [ + ".osano-cm-window" + ] + } + ] + }, + { + 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: true, + optional: true + }, + { + click: ".qc-cmp2-main button[aria-label=\"REJECT ALL\"]", + optional: true + }, + { + waitForThenClick: ".qc-cmp2-main button[aria-label=\"SAVE & EXIT\"],.qc-cmp2-buttons-desktop > button[mode=\"primary\"]", + timeout: 5000 + } + ], + optIn: [ + { + click: ".qc-cmp2-summary-buttons > button[mode=\"primary\"]" + } + ] + }, + { + name: "Tealium", + prehideSelectors: [ + "#__tealiumGDPRecModal,#__tealiumGDPRcpPrefs,#consent-layer" + ], + isHidingRule: false, + detectCmp: [ + { + exists: "#__tealiumGDPRecModal" + }, + { + "eval": "window.utag && typeof utag.gdpr === 'object'" + } + ], + detectPopup: [ + { + visible: "#__tealiumGDPRecModal" + } + ], + optOut: [ + { + hide: [ + "#__tealiumGDPRecModal", + "#__tealiumGDPRcpPrefs", + "#consent-layer" + ] + }, + { + click: "#cm-acceptNone,.js-accept-essential-cookies" + } + ], + optIn: [ + { + hide: [ + "#__tealiumGDPRecModal" + ] + }, + { + "eval": "utag.gdpr.setConsentValue(true)" + } + ], + test: [ + { + "eval": "utag.gdpr.getConsentState() !== 1" + } + ] + }, + { + name: "Test page CMP", + prehideSelectors: [ + "#reject-all" + ], + detectCmp: [ + { + exists: "#privacy-test-page-cmp-test" + } + ], + detectPopup: [ + { + visible: "#privacy-test-page-cmp-test" + } + ], + optIn: [ + { + click: "#accept-all" + } + ], + optOut: [ + { + waitFor: "#reject-all" + }, + { + click: "#reject-all" + } + ], + test: [ + { + "eval": "window.results.results[0] === 'button_clicked'" + } + ] + } + ]; + var consentomatic = { + "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: true, + 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: true, + 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: true, + 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" + } + ] + } + }; + var rules = { + autoconsent: autoconsent, + consentomatic: consentomatic + }; + + var rules$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + autoconsent: autoconsent, + consentomatic: consentomatic, + 'default': rules + }); + + /* global browser */ + + const consent = new AutoConsent(browser, browser.tabs.sendMessage); + + async function loadRules () { + console.log(rules$1); + Object.keys(consentomatic).forEach((name) => { + consent.addConsentomaticCMP(name, consentomatic[name]); + }); + autoconsent.forEach((rule) => { + consent.addCMP(rule); + }); + } + + loadRules(); + + browser.webNavigation.onCommitted.addListener((details) => { + if (details.frameId === 0) { + consent.removeTab(details.tabId); + } + }, { + url: [{ schemes: ['http', 'https'] }] + }); + + browser.webNavigation.onCompleted.addListener(consent.onFrame.bind(consent), { + url: [{ schemes: ['http', 'https'] }] + }); + + window.autoconsent = consent; + + window.callAction = (messageId, tabId, action) => { + const respond = (obj) => { + window.webkit.messageHandlers.actionResponse.postMessage(JSON.stringify({ + messageId, + ...obj + })).catch(() => console.warn('Error sending response', messageId, obj)); + }; + const errorResponse = (err) => { + console.warn('action error', err); + respond({ result: false, error: err.toString() }); + }; + + if (action === 'detectCMP') { + consent.checkTab(tabId).then(async (cmp) => { + try { + await cmp.checked; + respond({ + ruleName: cmp.getCMPName(), + result: cmp.getCMPName() !== null + }); + } catch (e) { + errorResponse(e); + } + }, errorResponse); + } else { + const cmp = consent.tabCmps.get(tabId); + if (!cmp) { + respond({ + result: false + }); + return + } + const successResponse = (result) => respond({ ruleName: cmp.getCMPName(), result }); + switch (action) { + case 'detectPopup': + cmp.isPopupOpen(20, 100).then(successResponse, errorResponse); + break + case 'doOptOut': + cmp.doOptOut().then(successResponse, errorResponse); + break + case 'selfTest': + if (!cmp.hasTest()) { + errorResponse('no test for this CMP'); + } else { + cmp.testOptOutWorked().then(successResponse, errorResponse); + } + break + } + } + return messageId + }; + +})(); diff --git a/DuckDuckGo/Autoconsent/background.js b/DuckDuckGo/Autoconsent/background.js new file mode 100644 index 0000000000..44aacff414 --- /dev/null +++ b/DuckDuckGo/Autoconsent/background.js @@ -0,0 +1,83 @@ +/* global browser */ +import AutoConsent from '@cliqz/autoconsent/lib/web' +import * as rules from '@cliqz/autoconsent/rules/rules.json' + +const consent = new AutoConsent(browser, browser.tabs.sendMessage) + +async function loadRules () { + console.log(rules) + Object.keys(rules.consentomatic).forEach((name) => { + consent.addConsentomaticCMP(name, rules.consentomatic[name]) + }) + rules.autoconsent.forEach((rule) => { + consent.addCMP(rule) + }) +} + +loadRules() + +browser.webNavigation.onCommitted.addListener((details) => { + if (details.frameId === 0) { + consent.removeTab(details.tabId) + } +}, { + url: [{ schemes: ['http', 'https'] }] +}) + +browser.webNavigation.onCompleted.addListener(consent.onFrame.bind(consent), { + url: [{ schemes: ['http', 'https'] }] +}) + +window.autoconsent = consent + +window.callAction = (messageId, tabId, action) => { + const respond = (obj) => { + window.webkit.messageHandlers.actionResponse.postMessage(JSON.stringify({ + messageId, + ...obj + })).catch(() => console.warn('Error sending response', messageId, obj)) + } + const errorResponse = (err) => { + console.warn('action error', err) + respond({ result: false, error: err.toString() }) + } + + if (action === 'detectCMP') { + consent.checkTab(tabId).then(async (cmp) => { + try { + await cmp.checked + respond({ + ruleName: cmp.getCMPName(), + result: cmp.getCMPName() !== null + }) + } catch (e) { + errorResponse(e) + } + }, errorResponse) + } else { + const cmp = consent.tabCmps.get(tabId) + if (!cmp) { + respond({ + result: false + }) + return + } + const successResponse = (result) => respond({ ruleName: cmp.getCMPName(), result }) + switch (action) { + case 'detectPopup': + cmp.isPopupOpen(20, 100).then(successResponse, errorResponse) + break + case 'doOptOut': + cmp.doOptOut().then(successResponse, errorResponse) + break + case 'selfTest': + if (!cmp.hasTest()) { + errorResponse('no test for this CMP') + } else { + cmp.testOptOutWorked().then(successResponse, errorResponse) + } + break + } + } + return messageId +} diff --git a/DuckDuckGo/Autoconsent/browser-shim.js b/DuckDuckGo/Autoconsent/browser-shim.js new file mode 100644 index 0000000000..f0e146645e --- /dev/null +++ b/DuckDuckGo/Autoconsent/browser-shim.js @@ -0,0 +1,77 @@ +let _msgCtr = 0 +const tabStore = new Map() + +class WebNavigationListener { + constructor () { + this._listeners = [] + } + + _trigger (args) { + this._listeners.forEach(({ fn }) => fn(args)) + } + + addListener (fn, filter) { + this._listeners.push({ fn, filter }) + } +} + +window.browser = { + webNavigation: { + onCommitted: new WebNavigationListener(), + onCompleted: new WebNavigationListener() + }, + tabs: { + get (tabId) { + return Promise.resolve(tabStore.get(tabId)) + }, + sendMessage: (tabId, message, { frameId } = { frameId: 0 }) => { + const messageId = _msgCtr++ + return window.webkit.messageHandlers.browserTabsMessage.postMessage(JSON.stringify({ + messageId, + tabId, + message, + frameId + })) + } + }, + runtime: { + onMessage: { + _listeners: [], + _trigger (...args) { + window.browser.runtime.onMessage._listeners.forEach((fn) => fn(...args)) + }, + addListener (cb) { + window.browser.runtime.onMessage._listeners.push(cb) + } + } + } +} + +window._nativeMessageHandler = (tabId, frameId, message) => { + // console.log(tabId, frameId, message) + switch (message.type) { + case 'webNavigation.onCommitted': + return window.browser.webNavigation.onCommitted._trigger({ + tabId, + frameId, + url: message.url, + timeStamp: Date.now() + }) + case 'webNavigation.onCompleted': + return window.browser.webNavigation.onCompleted._trigger({ + tabId, + frameId, + url: message.url, + timeStamp: Date.now() + }) + case 'runtime.sendMessage': + return window.browser.runtime.onMessage._trigger(message.payload, { + tab: { + id: tabId + }, + frameId + }) + } +} + +setTimeout(() => window.webkit.messageHandlers.ready.postMessage({}), 50) diff --git a/DuckDuckGo/Autoconsent/userscript.js b/DuckDuckGo/Autoconsent/userscript.js new file mode 100644 index 0000000000..38749bc1ca --- /dev/null +++ b/DuckDuckGo/Autoconsent/userscript.js @@ -0,0 +1,27 @@ +import handleContentMessage from '@cliqz/autoconsent/lib/web/content' + +window.autoconsent = (payload) => { + return handleContentMessage(payload.message, false) +} + +window.webkit.messageHandlers.autoconsentBackgroundMessage.postMessage(JSON.stringify({ + type: 'webNavigation.onCommitted', + url: window.location.href +})) + +const isMainDocument = window === window.top +if (isMainDocument) { + setTimeout(() => { + window.webkit.messageHandlers.autoconsentPageReady.postMessage(window.location.href) + }, 100) +} + +window.onload = () => { + window.webkit.messageHandlers.autoconsentBackgroundMessage.postMessage(JSON.stringify({ + type: 'webNavigation.onCompleted', + url: window.location.href + })) + if (isMainDocument) { + window.webkit.messageHandlers.autoconsentPageReady.postMessage(window.location.href) + } +} diff --git a/DuckDuckGo/BrowserTab/Model/Tab.swift b/DuckDuckGo/BrowserTab/Model/Tab.swift index 39de55b8d7..0aafc16775 100644 --- a/DuckDuckGo/BrowserTab/Model/Tab.swift +++ b/DuckDuckGo/BrowserTab/Model/Tab.swift @@ -473,6 +473,7 @@ final class Tab: NSObject { userScripts.pageObserverScript.delegate = self userScripts.printingUserScript.delegate = self userScripts.hoverUserScript.delegate = self + userScripts.autoconsentUserScript?.delegate = self attachFindInPage() @@ -535,7 +536,8 @@ final class Tab: NSObject { @Published var trackerInfo: TrackerInfo? @Published var serverTrust: ServerTrust? @Published var connectionUpgradedTo: URL? - + @Published var cookieConsentManaged: CookieConsentInfo? + public func resetDashboardInfo(_ url: URL?) { trackerInfo = TrackerInfo() if self.serverTrust?.host != url?.host { @@ -1012,5 +1014,11 @@ extension Tab: HoverUserScriptDelegate { } +@available(macOS 11, *) +extension Tab: AutoconsentUserScriptDelegate { + func autoconsentUserScript(consentStatus: CookieConsentInfo) { + self.cookieConsentManaged = consentStatus + } +} // swiftlint:enable type_body_length // swiftlint:enable file_length diff --git a/DuckDuckGo/BrowserTab/Model/UserScripts.swift b/DuckDuckGo/BrowserTab/Model/UserScripts.swift index 46993a6871..a2363efc7e 100644 --- a/DuckDuckGo/BrowserTab/Model/UserScripts.swift +++ b/DuckDuckGo/BrowserTab/Model/UserScripts.swift @@ -35,6 +35,7 @@ final class UserScripts { let surrogatesScript: SurrogatesUserScript let contentScopeUserScript: ContentScopeUserScript let autofillScript: AutofillUserScript + let autoconsentUserScript: UserScriptWithAutoconsent? init(with sourceProvider: ScriptSourceProviding) { @@ -45,6 +46,12 @@ final class UserScripts { let prefs = ContentScopeProperties.init(gpcEnabled: privacySettings.gpcEnabled, sessionKey: sessionKey) contentScopeUserScript = ContentScopeUserScript(sourceProvider.privacyConfigurationManager, properties: prefs) autofillScript = AutofillUserScript(scriptSourceProvider: sourceProvider.autofillSourceProvider!) + if #available(macOS 11, *) { + autoconsentUserScript = AutoconsentUserScript() + userScripts.append(autoconsentUserScript!) + } else { + autoconsentUserScript = nil + } } lazy var userScripts: [UserScript] = [ diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index 7a3678c197..7ab9bf7f72 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -117,5 +117,15 @@ extension NSAlert { alert.addButton(withTitle: UserText.ok) return alert } + + static func cookiePopup() -> NSAlert { + let alert = NSAlert() + alert.messageText = UserText.autoconsentPopupTitle + alert.informativeText = UserText.autoconsentPopupDescription + alert.addButton(withTitle: UserText.autoconsentPopupEnableButton) + alert.addButton(withTitle: UserText.autoconsentPopupLaterButton) + alert.addButton(withTitle: UserText.autoconsentPopupNeverButton) + return alert + } } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 635a994870..98cc29bfb3 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -396,4 +396,11 @@ struct UserText { static let importFromFirefoxMoreInfo = NSLocalizedString("import.from.firefox.info", value: "You'll be asked to enter your Primary Password for Firefox.\n\nImported passwords are encrypted and only stored on this computer.", comment: "More info when importing from Firefox") + + static let autoconsentPopoverMessage = NSLocalizedString("Cookie consent pop-up managed", comment: "Popover message") + static let autoconsentPopupTitle = NSLocalizedString("Let DuckDuckGo try to manage cookie consent pop-ups?", comment: "messageText") + static let autoconsentPopupDescription = NSLocalizedString("On some sites, we can automatically set preferences to minimize cookies 🍪 and maximize privacy, then close the pop-up.", comment: "informativeText") + static let autoconsentPopupEnableButton = NSLocalizedString("Manage Cookie Pop-ups", comment: "") + static let autoconsentPopupLaterButton = NSLocalizedString("Not Now", comment: "") + static let autoconsentPopupNeverButton = NSLocalizedString("Don't Ask Again", comment: "") } diff --git a/DuckDuckGo/Common/Utilities/Logging.swift b/DuckDuckGo/Common/Utilities/Logging.swift index d647f2681d..546a41cef9 100644 --- a/DuckDuckGo/Common/Utilities/Logging.swift +++ b/DuckDuckGo/Common/Utilities/Logging.swift @@ -43,6 +43,10 @@ extension OSLog { static var pixel: OSLog { Logging.pixelLoggingEnabled ? Logging.pixelLog : .disabled } + + static var autoconsent: OSLog { + Logging.autoconsentLoggingEnabled ? Logging.autoconsentLog : .disabled + } static var contentBlocking: OSLog { Logging.contentBlockingLoggingEnabled ? Logging.contentBlockingLog : .disabled @@ -80,4 +84,7 @@ struct Logging { fileprivate static let faviconLoggingEnabled = false fileprivate static let faviconLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Favicons") + fileprivate static let autoconsentLoggingEnabled = false + fileprivate static let autoconsentLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Autoconsent") + } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 327f930c05..346f9bae4d 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -43,6 +43,7 @@ public struct UserDefaultsWrapper { case loginDetectionEnabled = "fireproofing.login-detection-enabled" case gpcEnabled = "preferences.gpc-enabled" case alwaysRequestDownloadLocationKey = "preferences.download-location.always-request" + case autoconsentEnabled = "preferences.autoconsent-enabled" case saveAsPreferredFileType = "saveAs.selected.filetype" diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 3a25ab9488..5dfb04376a 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -28,6 +28,7 @@ final class Fire { let downloadListCoordinator: DownloadListCoordinator let windowControllerManager: WindowControllersManager let faviconManagement: FaviconManagement + let autoconsentManagement: AutoconsentManagement? @Published private(set) var isBurning = false @@ -36,13 +37,20 @@ final class Fire { permissionManager: PermissionManagerProtocol = PermissionManager.shared, downloadListCoordinator: DownloadListCoordinator = DownloadListCoordinator.shared, windowControllerManager: WindowControllersManager = WindowControllersManager.shared, - faviconManagement: FaviconManagement = FaviconManager.shared) { + faviconManagement: FaviconManagement = FaviconManager.shared, + autoconsentManagement: AutoconsentManagement? = nil) { self.webCacheManager = cacheManager self.historyCoordinating = historyCoordinating self.permissionManager = permissionManager self.downloadListCoordinator = downloadListCoordinator self.windowControllerManager = windowControllerManager self.faviconManagement = faviconManagement + + if #available(macOS 11, *), autoconsentManagement == nil { + self.autoconsentManagement = AutoconsentUserScript.background + } else { + self.autoconsentManagement = autoconsentManagement + } } func burnDomains(_ domains: Set, completion: (() -> Void)? = nil) { @@ -80,6 +88,8 @@ final class Fire { burnTabs(relatedTo: burningDomains, completion: { group.leave() }) + + burnAutoconsentCache() group.notify(queue: .main) { self.isBurning = false @@ -115,6 +125,8 @@ final class Fire { burnWindows(exceptOwnerOf: tabCollectionViewModel) { group.leave() } + + burnAutoconsentCache() group.notify(queue: .main) { self.isBurning = false @@ -220,6 +232,13 @@ final class Fire { completion() } } + + // MARK: - Autoconsent visit cache + private func burnAutoconsentCache() { + if #available(macOS 11, *), self.autoconsentManagement != nil { + self.autoconsentManagement!.clearCache() + } + } } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 30c64800eb..86b524eae8 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -210,6 +210,12 @@ final class NavigationBarViewController: NSViewController { selector: #selector(showPrivateEmailCopiedToClipboard(_:)), name: Notification.Name.privateEmailCopiedToClipboard, object: nil) + if #available(macOS 11, *) { + NotificationCenter.default.addObserver(self, + selector: #selector(showAutoconsentFeedback(_:)), + name: AutoconsentUserScript.Constants.newSitePopupHidden, + object: nil) + } } @objc private func showPrivateEmailCopiedToClipboard(_ sender: Notification) { @@ -231,6 +237,21 @@ final class NavigationBarViewController: NSViewController { viewController.show(onParent: self, relativeTo: self.optionsButton) } } + + @objc private func showAutoconsentFeedback(_ sender: Notification) { + if #available(macOS 11, *) { + guard view.window?.isKeyWindow == true, + let host = sender.userInfo?[AutoconsentUserScript.Constants.popupHiddenHostKey] as? String, + !AutoconsentUserScript.background.sitesNotifiedCache.contains(host), + let relativeTarget = self.addressBarViewController?.addressBarButtonsViewController?.privacyEntryPointButton + else { return } + AutoconsentUserScript.background.sitesNotifiedCache.insert(host) + DispatchQueue.main.async { + let viewController = PopoverMessageViewController.createWithMessage(UserText.autoconsentPopoverMessage) + viewController.show(onParent: self, relativeTo: relativeTarget) + } + } + } func closeTransientPopovers() -> Bool { guard !saveCredentialsPopover.isShown else { diff --git a/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift b/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift index 4e26106e98..6ee4a17efa 100644 --- a/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift +++ b/DuckDuckGo/Preferences/Model/PrivacySecurityPreferences.swift @@ -30,6 +30,13 @@ struct PrivacySecurityPreferences { GPCRequestFactory.shared.reloadGPCSetting() } } + + // This setting is an optional boolean as it has three states: + // - nil: User has not chosen a setting + // - true: Enabled by the user + // - false: Disabled by the user + @UserDefaultsWrapper(key: .autoconsentEnabled, defaultValue: nil) + public var autoconsentEnabled: Bool? } extension PrivacySecurityPreferences: PreferenceSection { diff --git a/DuckDuckGo/Preferences/View/Preferences.storyboard b/DuckDuckGo/Preferences/View/Preferences.storyboard index 23637c31f1..ded371a57f 100644 --- a/DuckDuckGo/Preferences/View/Preferences.storyboard +++ b/DuckDuckGo/Preferences/View/Preferences.storyboard @@ -211,7 +211,7 @@ - + @@ -348,9 +348,9 @@ - + - + @@ -382,7 +382,7 @@ - +