From 16efa5aee459a2195b521d91395456c81e6bda41 Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Wed, 11 Dec 2024 15:35:37 -0500 Subject: [PATCH] Add feature flag to display blocked requests per-tab in shields panel on iOS. --- .../brave_shields/core/common/features.cc | 5 ++ .../brave_shields/core/common/features.h | 1 + .../AdblockBlockedRequestsView.swift | 74 +++++++++++++++++++ .../Frontend/Shields/ShieldsPanelView.swift | 21 ++++++ .../Paged/ContentBlockerScriptHandler.swift | 14 +++- .../RequestBlockingContentScriptHandler.swift | 14 +++- .../ContentBlocker/ContentBlockerHelper.swift | 31 +++++++- .../Sources/BraveShields/ShieldStrings.swift | 69 +++++++++++++++++ ios/browser/api/features/features.h | 1 + ios/browser/api/features/features.mm | 5 ++ ios/browser/flags/about_flags.mm | 7 ++ 11 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdblockBlockedRequestsView.swift diff --git a/components/brave_shields/core/common/features.cc b/components/brave_shields/core/common/features.cc index 155a271f32d3..63bd9c0219f4 100644 --- a/components/brave_shields/core/common/features.cc +++ b/components/brave_shields/core/common/features.cc @@ -107,6 +107,11 @@ BASE_FEATURE(kBraveShredCacheData, #else base::FEATURE_DISABLED_BY_DEFAULT); #endif +// When enabled, will display debug menu for adblock features in the Shields +// panel. +BASE_FEATURE(kBraveIOSDebugAdblock, + "BraveIOSDebugAdblock", + base::FEATURE_DISABLED_BY_DEFAULT); // When enabled, show Strict (aggressive) fingerprinting mode in Brave Shields. BASE_FEATURE(kBraveShowStrictFingerprintingMode, "BraveShowStrictFingerprintingMode", diff --git a/components/brave_shields/core/common/features.h b/components/brave_shields/core/common/features.h index 0714290d1558..cc18e324df98 100644 --- a/components/brave_shields/core/common/features.h +++ b/components/brave_shields/core/common/features.h @@ -34,6 +34,7 @@ BASE_DECLARE_FEATURE(kBraveLocalhostAccessPermission); BASE_DECLARE_FEATURE(kBraveReduceLanguage); BASE_DECLARE_FEATURE(kBraveShredFeature); BASE_DECLARE_FEATURE(kBraveShredCacheData); +BASE_DECLARE_FEATURE(kBraveIOSDebugAdblock); BASE_DECLARE_FEATURE(kBraveShowStrictFingerprintingMode); BASE_DECLARE_FEATURE(kCosmeticFilteringExtraPerfMetrics); BASE_DECLARE_FEATURE(kCosmeticFilteringJsPerformance); diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdblockBlockedRequestsView.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdblockBlockedRequestsView.swift new file mode 100644 index 000000000000..5349ae5e1b87 --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/AdblockBlockedRequestsView.swift @@ -0,0 +1,74 @@ +// Copyright 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveShields +import Strings +import SwiftUI + +struct AdblockBlockedRequestsView: View { + + let url: String + @ObservedObject var contentBlockerHelper: ContentBlockerHelper + + @State private var filterText: String = "" + + private var blockedRequests: [BlockedRequestInfo] { + let blockedRequests = Array(contentBlockerHelper.blockedRequests) + guard !filterText.isEmpty else { + return blockedRequests + } + return blockedRequests.filter { + $0.requestURL.absoluteString.localizedCaseInsensitiveContains(filterText) + || $0.sourceURL.absoluteString.localizedCaseInsensitiveContains(filterText) + || $0.resourceType.rawValue.localizedCaseInsensitiveContains(filterText) + || $0.location.display.localizedCaseInsensitiveContains(filterText) + } + } + + public var body: some View { + List { + Section(header: Text(url)) { + ForEach(blockedRequests) { request in + VStack { + row( + title: String.localizedStringWithFormat("%@:", Strings.Shields.requestURLLabel), + detail: request.requestURL.absoluteString + ) + row( + title: String.localizedStringWithFormat("%@:", Strings.Shields.sourceURLLabel), + detail: request.sourceURL.absoluteString + ) + row( + title: String.localizedStringWithFormat("%@:", Strings.Shields.resourceTypeLabel), + detail: request.resourceType.rawValue + ) + row( + title: String.localizedStringWithFormat("%@:", Strings.Shields.aggressiveLabel), + detail: "\(request.isAggressive)" + ) + row( + title: String.localizedStringWithFormat("%@:", Strings.Shields.blockedByLabel), + detail: request.location.display + ) + } + } + } + } + .navigationTitle(Strings.Shields.blockedRequestsTitle) + .toolbar(.visible) + .searchable(text: $filterText) + } + + @ViewBuilder private func row(title: String, detail: String) -> some View { + Group { + Text(title) + Text(detail) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + } + .font(.body) + .frame(maxWidth: .infinity, alignment: .leading) + } +} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Shields/ShieldsPanelView.swift b/ios/brave-ios/Sources/Brave/Frontend/Shields/ShieldsPanelView.swift index 83faf1088c20..060582202342 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Shields/ShieldsPanelView.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Shields/ShieldsPanelView.swift @@ -27,6 +27,7 @@ struct ShieldsPanelView: View { } private let url: URL + private weak var tab: Tab? private let displayHost: String @AppStorage("advancedShieldsExpanded") private var advancedShieldsExpanded = false @ObservedObject private var viewModel: ShieldsSettingsViewModel @@ -34,6 +35,7 @@ struct ShieldsPanelView: View { @MainActor init(url: URL, tab: Tab, domain: Domain, callback: @escaping (Action) -> Void) { self.url = url + self.tab = tab self.viewModel = ShieldsSettingsViewModel(tab: tab, domain: domain) self.actionCallback = callback self.displayHost = @@ -251,6 +253,25 @@ struct ShieldsPanelView: View { .padding(.vertical, 4) } } + if FeatureList.kBraveIOSDebugAdblock.enabled, let contentBlocker = tab?.contentBlocker { + ShieldSettingRow { + NavigationLink { + AdblockBlockedRequestsView( + url: url.baseDomain ?? url.absoluteDisplayString, + contentBlockerHelper: contentBlocker + ) + } label: { + ShieldSettingsNavigationWrapper { + Text(Strings.Shields.blockedRequestsTitle) + .frame(maxWidth: .infinity, alignment: .leading) + .multilineTextAlignment(.leading) + } + } + .foregroundStyle(Color(.bravePrimary)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } + } } @ViewBuilder private var globalSettingsSectionView: some View { diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift index 95162142be31..c4f4006dfb47 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/ContentBlockerScriptHandler.swift @@ -136,8 +136,18 @@ extension ContentBlockerHelper: TabContentScript { } // First check to make sure we're not counting the same repetitive requests multiple times - guard !self.blockedRequests.contains(requestURL) else { return } - self.blockedRequests.insert(requestURL) + guard !self.blockedRequests.contains(where: { $0.requestURL == requestURL }) else { + return + } + self.blockedRequests.append( + .init( + requestURL: requestURL, + sourceURL: sourceURL, + resourceType: dto.resourceType, + isAggressive: domain.globalBlockAdsAndTrackingLevel.isAggressive, + location: .contentBlocker + ) + ) // Increase global stats (here due to BlocklistName being in Client and BraveGlobalShieldStats being // in BraveShared) diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift index c62caf930e2d..c017f21713e0 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Paged/RequestBlockingContentScriptHandler.swift @@ -109,11 +109,21 @@ class RequestBlockingContentScriptHandler: TabContentScript { ) } - if shouldBlock && !tab.contentBlocker.blockedRequests.contains(requestURL) { + if shouldBlock + && !tab.contentBlocker.blockedRequests.contains(where: { $0.requestURL == requestURL }) + { BraveGlobalShieldStats.shared.adblock += 1 let stats = tab.contentBlocker.stats tab.contentBlocker.stats = stats.adding(adCount: 1) - tab.contentBlocker.blockedRequests.insert(requestURL) + tab.contentBlocker.blockedRequests.append( + .init( + requestURL: requestURL, + sourceURL: windowOriginURL, + resourceType: dto.data.resourceType, + isAggressive: domain.globalBlockAdsAndTrackingLevel.isAggressive, + location: .requestBlocking + ) + ) } replyHandler(shouldBlock, nil) diff --git a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift index f67f01c29b2d..28004037ac8f 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerHelper.swift @@ -2,7 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +import BraveCore import BraveShared +import Collections import Combine import Data import Shared @@ -37,7 +39,32 @@ enum BlockingStrength: String { static let allOptions: [BlockingStrength] = [.basic, .strict] } -class ContentBlockerHelper { +struct BlockedRequestInfo: Hashable, Identifiable { + enum Location: String { + case contentBlocker + case requestBlocking + + var display: String { + switch self { + case .contentBlocker: + return Strings.Shields.contentBlocker + case .requestBlocking: + return Strings.Shields.requestBlocking + } + } + } + let requestURL: URL + let sourceURL: URL + let resourceType: AdblockEngine.ResourceType + let isAggressive: Bool + let location: Location + + var id: String { + "\(requestURL)\(sourceURL)\(resourceType.rawValue)\(isAggressive)\(location.rawValue)" + } +} + +class ContentBlockerHelper: ObservableObject { private(set) weak var tab: Tab? /// The rule lists that are loaded into the current tab @@ -54,7 +81,7 @@ class ContentBlockerHelper { } var statsDidChange: ((TPPageStats) -> Void)? - var blockedRequests: Set = [] + @Published var blockedRequests: OrderedSet = [] init(tab: Tab) { self.tab = tab diff --git a/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift b/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift index c32edf165088..e6210432e911 100644 --- a/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift +++ b/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift @@ -113,6 +113,75 @@ extension Strings.Shields { ) } +// MARK: - Adblock Debugging + +extension Strings.Shields { + public static let blockedRequestsTitle = NSLocalizedString( + "blockedRequestsTitle", + tableName: "BraveShared", + bundle: .module, + value: "Blocked Requests", + comment: + "The title displayed in the navigation bar of Blocked Requests view." + ) + public static let requestURLLabel = NSLocalizedString( + "requestURLLabel", + tableName: "BraveShared", + bundle: .module, + value: "Request URL", + comment: + "A label displayed above the request url that was blocked in Blocked Requests view." + ) + public static let sourceURLLabel = NSLocalizedString( + "sourceURLLabel", + tableName: "BraveShared", + bundle: .module, + value: "Source URL", + comment: + "A label displayed above the source url of a request that was blocked in Blocked Requests view." + ) + public static let resourceTypeLabel = NSLocalizedString( + "resourceTypeLabel", + tableName: "BraveShared", + bundle: .module, + value: "Resource Type", + comment: + "A label displayed above the resource type of a request that was blocked in Blocked Requests view." + ) + public static let aggressiveLabel = NSLocalizedString( + "aggressiveLabel", + tableName: "BraveShared", + bundle: .module, + value: "Aggressive", + comment: + "A label displayed above a value indicating if the site is in aggressive mode in Blocked Requests view." + ) + public static let blockedByLabel = NSLocalizedString( + "blockedByLabel", + tableName: "BraveShared", + bundle: .module, + value: "Blocked By", + comment: + "A label displayed above the location a request was blocked in Blocked Requests view." + ) + public static let contentBlocker = NSLocalizedString( + "contentBlocker", + tableName: "BraveShared", + bundle: .module, + value: "Content Blocker", + comment: + "Used to describe when a request was blocked by the Content Blocker in Blocked Requests view." + ) + public static let requestBlocking = NSLocalizedString( + "requestBlocking", + tableName: "BraveShared", + bundle: .module, + value: "Request Blocking", + comment: + "Used to describe when a request was blocked by our request blocking scripts in Blocked Requests view." + ) +} + // MARK: - Anti Ad-Block Warning extension Strings.Shields { diff --git a/ios/browser/api/features/features.h b/ios/browser/api/features/features.h index 07d3451c9497..e2c7f494d714 100644 --- a/ios/browser/api/features/features.h +++ b/ios/browser/api/features/features.h @@ -52,6 +52,7 @@ OBJC_EXPORT @property(class, nonatomic, readonly) Feature* kBraveSearchDefaultAPIFeature; @property(class, nonatomic, readonly) Feature* kBraveShredFeature; @property(class, nonatomic, readonly) Feature* kBraveShredCacheData; +@property(class, nonatomic, readonly) Feature* kBraveIOSDebugAdblock; @property(class, nonatomic, readonly) Feature* kBraveShowStrictFingerprintingMode; @property(class, nonatomic, readonly) Feature* kBraveSync; diff --git a/ios/browser/api/features/features.mm b/ios/browser/api/features/features.mm index 4633e57ee0f3..4c646d0f57fd 100644 --- a/ios/browser/api/features/features.mm +++ b/ios/browser/api/features/features.mm @@ -206,6 +206,11 @@ + (Feature*)kBraveShredCacheData { initWithFeature:&brave_shields::features::kBraveShredCacheData]; } ++ (Feature*)kBraveIOSDebugAdblock { + return [[Feature alloc] + initWithFeature:&brave_shields::features::kBraveIOSDebugAdblock]; +} + + (Feature*)kBraveShowStrictFingerprintingMode { return [[Feature alloc] initWithFeature:&brave_shields::features:: diff --git a/ios/browser/flags/about_flags.mm b/ios/browser/flags/about_flags.mm index 126aa83a44ed..ee8e6688e509 100644 --- a/ios/browser/flags/about_flags.mm +++ b/ios/browser/flags/about_flags.mm @@ -113,6 +113,13 @@ flags_ui::kOsIos, \ FEATURE_VALUE_TYPE( \ security_interstitials::features::kHttpsOnlyMode), \ + }, \ + { \ + "ios-debug-adblock", \ + "Enable Debug Adblock views", \ + "Enable debug view for adblock features in Shields panel", \ + flags_ui::kOsIos, \ + FEATURE_VALUE_TYPE(brave_shields::features::kBraveIOSDebugAdblock), \ }) #define BRAVE_AI_CHAT_FEATURE_ENTRIES \