From a6b7ba151d9dc6684484f3785293875ec01cc1ff Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Wed, 6 Dec 2023 08:58:23 -0600 Subject: [PATCH] Ignore previous rules for popups in top-frame (#18) * Ignore previous rules for popups in top-frame * Lint. Add test to assert new rule * Lint --- .../TrackerRadarKit/ContentBlockerRule.swift | 30 +++++++++++++++---- .../ContentBlockerRulesBuilder.swift | 16 ++++++++-- .../ContentBlockerRulesBuilderTests.swift | 17 +++++++++++ .../Test Utilities/ArrayExtensions.swift | 4 +++ 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/Sources/TrackerRadarKit/ContentBlockerRule.swift b/Sources/TrackerRadarKit/ContentBlockerRule.swift index fbecfa5..271f09b 100644 --- a/Sources/TrackerRadarKit/ContentBlockerRule.swift +++ b/Sources/TrackerRadarKit/ContentBlockerRule.swift @@ -45,11 +45,17 @@ public struct ContentBlockerRule: Codable, Hashable { } + public enum LoadContext: String, Codable { + case topFrame = "top-frame" + case childFrame = "child-frame" + } + let urlFilter: String let unlessDomain: [String]? let ifDomain: [String]? let resourceType: [ResourceType]? let loadType: [LoadType]? + let loadContext: [LoadContext]? enum CodingKeys: String, CodingKey { case urlFilter = "url-filter" @@ -57,40 +63,52 @@ public struct ContentBlockerRule: Codable, Hashable { case ifDomain = "if-domain" case resourceType = "resource-type" case loadType = "load-type" + case loadContext = "load-context" } - private init(urlFilter: String, unlessDomain: [String]?, ifDomain: [String]?, resourceType: [ResourceType]?, loadType: [LoadType]?) { + private init(urlFilter: String, unlessDomain: [String]?, ifDomain: [String]?, + resourceType: [ResourceType]?, loadType: [LoadType]?, loadContext: [LoadContext]?) { self.urlFilter = urlFilter self.unlessDomain = unlessDomain self.ifDomain = ifDomain self.resourceType = resourceType self.loadType = loadType + self.loadContext = loadContext } public static func trigger(onDomain domain: String) -> Trigger { return Trigger(urlFilter: ContentBlockerRulesBuilder.Constants.subDomainPrefix + domain.replacingOccurrences(of: ".", with: "\\.") + ContentBlockerRulesBuilder.Constants.domainMatchSuffix, - unlessDomain: nil, ifDomain: nil, resourceType: nil, loadType: nil) + unlessDomain: nil, ifDomain: nil, resourceType: nil, loadType: nil, loadContext: nil) } public static func trigger(urlFilter filter: String, loadTypes: [LoadType]? = [ .thirdParty ]) -> Trigger { - return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: nil, resourceType: nil, loadType: loadTypes) + return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: nil, resourceType: nil, loadType: loadTypes, loadContext: nil) } public static func trigger(urlFilter filter: String, unlessDomain urls: [String]?, loadTypes: [LoadType]? = [ .thirdParty ] ) -> Trigger { - return Trigger(urlFilter: filter, unlessDomain: urls, ifDomain: nil, resourceType: nil, loadType: loadTypes) + return Trigger(urlFilter: filter, unlessDomain: urls, ifDomain: nil, resourceType: nil, loadType: loadTypes, loadContext: nil) } public static func trigger(urlFilter filter: String, ifDomain domains: [String]?, resourceType types: [ResourceType]?) -> Trigger { - return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: domains, resourceType: types, loadType: [ .thirdParty ]) + return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: domains, resourceType: types, loadType: [ .thirdParty ], loadContext: nil) } public static func trigger(urlFilter filter: String, ifDomain domains: [String]?, resourceType types: [ResourceType]?, loadTypes: [LoadType]? = [ .thirdParty ]) -> Trigger { - return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: domains, resourceType: types, loadType: loadTypes) + return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: domains, resourceType: types, loadType: loadTypes, loadContext: nil) + } + + public static func trigger(urlFilter filter: String, + ifDomain domains: [String]?, + resourceType types: [ResourceType]?, + loadTypes: [LoadType]? = [ .thirdParty ], + loadContext: [LoadContext]? = nil) -> Trigger { + return Trigger(urlFilter: filter, unlessDomain: nil, ifDomain: domains, + resourceType: types, loadType: loadTypes, loadContext: loadContext) } } diff --git a/Sources/TrackerRadarKit/ContentBlockerRulesBuilder.swift b/Sources/TrackerRadarKit/ContentBlockerRulesBuilder.swift index 8b44ae4..4a3688c 100644 --- a/Sources/TrackerRadarKit/ContentBlockerRulesBuilder.swift +++ b/Sources/TrackerRadarKit/ContentBlockerRulesBuilder.swift @@ -167,7 +167,8 @@ public struct ContentBlockerRulesBuilder { ] } else if r.options == nil && r.exceptions == nil { return [ - block(r, withOwner: tracker.owner, loadTypes: loadTypes) + block(r, withOwner: tracker.owner, loadTypes: loadTypes), + ignorePrevious(r, resourceTypes: [.popup], loadTypes: loadTypes, loadContext: [.topFrame]) ] } else if r.exceptions != nil && r.options != nil { return [ @@ -245,7 +246,18 @@ public struct ContentBlockerRulesBuilder { loadTypes: loadTypes), action: .ignorePreviousRules()) } - + + private func ignorePrevious(_ rule: KnownTracker.Rule, matching: KnownTracker.Rule.Matching? = nil, + resourceTypes: [ContentBlockerRule.Trigger.ResourceType], loadTypes: [ContentBlockerRule.Trigger.LoadType], + loadContext: [ContentBlockerRule.Trigger.LoadContext]) -> ContentBlockerRule { + return ContentBlockerRule(trigger: .trigger(urlFilter: rule.normalizedRule(), + ifDomain: matching?.domains?.prefixAll(with: "*"), + resourceType: resourceTypes, + loadTypes: loadTypes, + loadContext: loadContext), + action: .ignorePreviousRules()) + } + } fileprivate extension String { diff --git a/Tests/TrackerRadarKitTests/ContentBlockerRulesBuilderTests.swift b/Tests/TrackerRadarKitTests/ContentBlockerRulesBuilderTests.swift index 19b3fa9..0e6ead8 100644 --- a/Tests/TrackerRadarKitTests/ContentBlockerRulesBuilderTests.swift +++ b/Tests/TrackerRadarKitTests/ContentBlockerRulesBuilderTests.swift @@ -47,6 +47,23 @@ class ContentBlockerRulesBuilderTests: XCTestCase { } } + func testDefaultIgnoreGeneratesPopupIgnoreRules() throws { + let rules = ContentBlockerRulesBuilder(trackerData: trackerData).buildRules(withExceptions: ["duckduckgo.com"], + andTemporaryUnprotectedDomains: []) + + // swiftlint:disable:next line_length + let domainFilter = "^(https?)?(wss?)?://([a-z0-9-]+\\.)*xvideos-cdn\\.com\\/v-c19d94e7937\\/v3\\/js\\/skins\\/min\\/default\\.header\\.static\\.js" + if let idx = rules.firstIndexOfExactFilter(filter: domainFilter) { + let nextRule = rules[idx + 1] + XCTAssertNotNil(nextRule, "Missing ignore-previous popup type rule") + XCTAssert(nextRule.action == .ignorePreviousRules()) + XCTAssert(nextRule.trigger.loadContext?.first == .topFrame) + XCTAssert(nextRule.trigger.resourceType?.first == .popup) + } else { + XCTFail("Missing rule for testing") + } + } + func testLoadingUnsupportedRules() throws { let data = JSONTestDataLoader.mockTrackerData guard let mockData = try? JSONDecoder().decode(TrackerData.self, from: data) else { diff --git a/Tests/TrackerRadarKitTests/Test Utilities/ArrayExtensions.swift b/Tests/TrackerRadarKitTests/Test Utilities/ArrayExtensions.swift index bfa707e..62b353a 100644 --- a/Tests/TrackerRadarKitTests/Test Utilities/ArrayExtensions.swift +++ b/Tests/TrackerRadarKitTests/Test Utilities/ArrayExtensions.swift @@ -35,4 +35,8 @@ extension Array where Element == ContentBlockerRule { return nil } + + func firstIndexOfExactFilter(filter: String) -> Int? { + self.firstIndex { $0.trigger.urlFilter == filter } + } }