From 92e62cb365869d3cc98923ac04c319f9f1f14b93 Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Wed, 10 May 2023 16:51:18 +0200 Subject: [PATCH] Fix #7352: Add tracking and ad block level setting --- .../Browser/BrowserViewController.swift | 1 + .../PlaylistCacheLoader.swift | 2 +- ...eShieldsAndPrivacySettingsController.swift | 38 +++- .../Shields/AdvancedShieldsView.swift | 75 ++++++- .../Shields/ShieldsViewController.swift | 20 +- .../SiteStateListenerScriptHandler.swift | 13 +- Sources/BraveShields/BraveShield.swift | 2 +- Sources/BraveStrings/BraveStrings.swift | 8 +- Sources/BraveUI/UIKit/SettingsRowViews.swift | 100 +++++++++ Sources/Data/models/Domain.swift | 90 +++++--- .../Model.xcdatamodeld/.xccurrentversion | 2 +- .../Model21.xcdatamodel/contents | 211 ++++++++++++++++++ Sources/Preferences/GlobalPreferences.swift | 72 ++++++ 13 files changed, 588 insertions(+), 46 deletions(-) create mode 100644 Sources/Data/models/Model.xcdatamodeld/Model21.xcdatamodel/contents diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController.swift index 47b731816a8..8607bf9f510 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController.swift @@ -2995,6 +2995,7 @@ extension BrowserViewController: PreferencesObserver { case Preferences.General.enablePullToRefresh.key: tabManager.selectedTab?.updatePullToRefreshVisibility() case Preferences.Shields.blockAdsAndTracking.key, + Preferences.Shields.blockAdsAndTrackingAggressive.key, Preferences.Shields.blockScripts.key, Preferences.Shields.blockPhishingAndMalware.key, Preferences.Shields.blockImages.key, diff --git a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift index facb23df19b..c0bddb4d18f 100644 --- a/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift +++ b/Sources/Brave/Frontend/Browser/Playlist/Managers & Cache/PlaylistCacheLoader.swift @@ -583,7 +583,7 @@ extension PlaylistWebLoader: WKNavigationDelegate { // Force adblocking on domainForShields.shield_allOff = 0 - domainForShields.shield_adblockAndTp = true + domainForShields.adBlockAndTPShieldLevel = .standard // Load block lists let ruleLists = await ContentBlockerManager.shared.ruleLists(for: domainForShields) diff --git a/Sources/Brave/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift b/Sources/Brave/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift index a088ccfa988..d4f5a118f58 100644 --- a/Sources/Brave/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift +++ b/Sources/Brave/Frontend/Settings/BraveShieldsAndPrivacySettingsController.swift @@ -105,14 +105,7 @@ class BraveShieldsAndPrivacySettingsController: TableViewController { // MARK: - P3A private func recordGlobalAdBlockShieldsP3A() { - // Q46 What is the global ad blocking shields setting? - enum Answer: Int, CaseIterable { - case disabled = 0 - case standard = 1 - case aggressive = 2 - } - let answer: Answer = Preferences.Shields.blockAdsAndTracking.value ? .standard : .disabled - UmaHistogramEnumeration("Brave.Shields.AdBlockSetting", sample: answer) + UmaHistogramEnumeration("Brave.Shields.AdBlockSetting", sample: Preferences.Shields.blockAdsAndTrackingLevel.p3AAnswer) } private func recordGlobalFingerprintingShieldsP3A() { @@ -132,7 +125,20 @@ class BraveShieldsAndPrivacySettingsController: TableViewController { var shields = Section( header: .title(Strings.shieldsDefaults), rows: [ - .boolRow(title: Strings.blockAdsAndTracking, detailText: Strings.blockAdsAndTrackingDescription, option: Preferences.Shields.blockAdsAndTracking), + .pickerRow( + title: Strings.trackersAndAdsBlocking, + detailText: Strings.trackersAndAdsBlockingDescription, + options: Preferences.Shields.ShieldLevel.allCases, + selectedValue: Preferences.Shields.blockAdsAndTrackingLevel, + valueChange: { value in + guard let shieldLevel = Preferences.Shields.ShieldLevel(rawValue: value.id) else { + assertionFailure() + return + } + + Preferences.Shields.blockAdsAndTrackingLevel = shieldLevel + } + ), .boolRow(title: Strings.HTTPSEverywhere, detailText: Strings.HTTPSEverywhereDescription, option: Preferences.Shields.httpsEverywhere), .boolRow(title: Strings.blockPhishingAndMalware, option: Preferences.Shields.blockPhishingAndMalware), .boolRow(title: Strings.autoRedirectAMPPages, detailText: Strings.autoRedirectAMPPagesDescription, option: Preferences.Shields.autoRedirectAMPPages), @@ -524,3 +530,17 @@ class BraveShieldsAndPrivacySettingsController: TableViewController { _toggleFolderAccessForBlockCookies(locked: true) } } + +extension Preferences.Shields.ShieldLevel: PickerAccessoryViewValue { + public var id: String { + return rawValue + } + + public var localizedTitle: String { + switch self { + case .aggressive: return Strings.trackersAndAdsBlockingAggressive + case .disabled: return Strings.trackersAndAdsBlockingDisabled + case .standard: return Strings.trackersAndAdsBlockingStandard + } + } +} diff --git a/Sources/Brave/Frontend/Shields/AdvancedShieldsView.swift b/Sources/Brave/Frontend/Shields/AdvancedShieldsView.swift index 16639fe86ae..60e6f6b5a04 100644 --- a/Sources/Brave/Frontend/Shields/AdvancedShieldsView.swift +++ b/Sources/Brave/Frontend/Shields/AdvancedShieldsView.swift @@ -6,10 +6,10 @@ import UIKit import Shared import BraveShared import BraveUI +import Preferences class AdvancedShieldsView: UIStackView { let siteTitle = HeaderTitleView() - let adsTrackersControl = ToggleView(title: Strings.blockAdsAndTracking) let blockMalwareControl = ToggleView(title: Strings.blockPhishing) let blockScriptsControl = ToggleView(title: Strings.blockScripts) let fingerprintingControl = ToggleView(title: Strings.fingerprintingProtection) @@ -17,6 +17,21 @@ class AdvancedShieldsView: UIStackView { $0.titleLabel.text = Strings.Shields.globalControls.uppercased() } let globalControlsButton = ChangeGlobalDefaultsView() + var adsTrackerValueChange: ((Preferences.Shields.ShieldLevel) -> Void)? + + lazy var adsTrackersControl: PickerView = { + return PickerView( + title: Strings.trackersAndAdsBlocking, + options: Preferences.Shields.ShieldLevel.allCases, + selectedValue: Preferences.Shields.blockAdsAndTrackingLevel) { [weak self] value in + guard let shieldLevel = Preferences.Shields.ShieldLevel(rawValue: value.id) else { + assertionFailure() + return + } + + self?.adsTrackerValueChange?(shieldLevel) + } + }() override init(frame: CGRect) { super.init(frame: frame) @@ -145,6 +160,64 @@ extension AdvancedShieldsView { valueToggled?(toggleSwitch.isOn) } } + + /// A container displaying a toggle for the user + class PickerView: UIView { + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 15.0) + label.numberOfLines = 0 + label.textColor = .braveLabel + return label + }() + + let pickerView: PickerAccessoryView + var selectedValue: PickerAccessoryView.ValueChange? + + override var accessibilityLabel: String? { + get { titleLabel.accessibilityLabel } + set { assertionFailure() } // swiftlint:disable:this unused_setter_value + } + + override var accessibilityValue: String? { + get { pickerView.accessibilityValue } + set { assertionFailure() } // swiftlint:disable:this unused_setter_value + } + + init(title: String, options: [PickerAccessoryViewValue], selectedValue: PickerAccessoryViewValue, valueChange: @escaping PickerAccessoryView.ValueChange) { + self.pickerView = PickerAccessoryView( + options: options, + selectedValue: selectedValue, + valueChange: valueChange + ) + + super.init(frame: .zero) + + let stackView = UIStackView(arrangedSubviews: [titleLabel, pickerView]) + stackView.spacing = 12.0 + stackView.alignment = .center + stackView.distribution = .equalSpacing + addSubview(stackView) + + snp.makeConstraints { + $0.height.greaterThanOrEqualTo(44) + } + + stackView.snp.makeConstraints { + $0.horizontalEdges.equalToSuperview().inset(16) + $0.verticalEdges.equalToSuperview() + } + + titleLabel.text = title + isAccessibilityElement = true + accessibilityTraits.insert(.button) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError() + } + } class SeparatorView: UIView { override init(frame: CGRect) { diff --git a/Sources/Brave/Frontend/Shields/ShieldsViewController.swift b/Sources/Brave/Frontend/Shields/ShieldsViewController.swift index 75354f1632f..99bf1190c2a 100644 --- a/Sources/Brave/Frontend/Shields/ShieldsViewController.swift +++ b/Sources/Brave/Frontend/Shields/ShieldsViewController.swift @@ -68,8 +68,10 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { if let domain = domain { shieldsUpSwitch.isOn = !domain.isShieldExpected(.AllOff, considerAllShieldsOption: false) + shieldsView.advancedShieldView.adsTrackersControl.pickerView.selectedValue = domain.adBlockAndTPShieldLevel } else { shieldsUpSwitch.isOn = true + shieldsView.advancedShieldView.adsTrackersControl.pickerView.selectedValue = Preferences.Shields.blockAdsAndTrackingLevel } shieldControlMapping.forEach { shield, view, option in @@ -85,6 +87,7 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { view.toggleSwitch.isOn = domain.isShieldExpected(shield, considerAllShieldsOption: false) } } + updateGlobalShieldState(shieldsUpSwitch.isOn) } @@ -212,7 +215,6 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { /// Groups the shield types with their control and global preference private lazy var shieldControlMapping: [(BraveShield, AdvancedShieldsView.ToggleView, Preferences.Option?)] = [ - (.AdblockAndTp, shieldsView.advancedShieldView.adsTrackersControl, Preferences.Shields.blockAdsAndTracking), (.SafeBrowsing, shieldsView.advancedShieldView.blockMalwareControl, Preferences.Shields.blockPhishingAndMalware), (.NoScript, shieldsView.advancedShieldView.blockScriptsControl, Preferences.Shields.blockScripts), (.FpProtection, shieldsView.advancedShieldView.fingerprintingControl, Preferences.Shields.fingerprintingProtection), @@ -264,6 +266,22 @@ class ShieldsViewController: UIViewController, PopoverContentComponent { shieldsView.advancedControlsBar.isShowingAdvancedControls = true updatePreferredContentSize() } + + shieldsView.advancedShieldView.adsTrackerValueChange = { [weak self] shieldLevel in + guard let self = self, let url = self.url else { return } + + Domain.setAdAndTP( + shieldLevel: shieldLevel, + for: url, + isPrivateBrowsing: PrivateBrowsingManager.shared.isPrivateBrowsing + ) + + // Wait a fraction of a second to allow DB write to complete otherwise it will not use the + // updated shield settings when reloading the page + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.shieldsSettingsChanged?(self, .AdblockAndTp) + } + } shieldControlMapping.forEach { shield, toggle, option in toggle.valueToggled = { [weak self] on in diff --git a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift index 81941879f6b..b8fd83eccd4 100644 --- a/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift +++ b/Sources/Brave/Frontend/UserContent/UserScripts/Scripts_Dynamic/ScriptHandlers/Sandboxed/SiteStateListenerScriptHandler.swift @@ -6,6 +6,7 @@ import Foundation import WebKit import Shared +import Preferences import os.log class SiteStateListenerScriptHandler: TabContentScript { @@ -67,8 +68,12 @@ class SiteStateListenerScriptHandler: TabContentScript { if domain.areAllShieldsOff { return } let models = await AdBlockStats.shared.cosmeticFilterModels(forFrameURL: frameURL, domain: domain) - let args = try self.makeArgs(from: models, frameURL: frameURL) - let source = try ScriptFactory.shared.makeScriptSource(of: .selectorsPoller).replacingOccurrences(of: "$", with: args) + let args = try self.makeArgs( + from: models, frameURL: frameURL, + isAggressive: domain.adBlockAndTPShieldLevel.isAggressive + ) + let source = try ScriptFactory.shared.makeScriptSource(of: .selectorsPoller) + .replacingOccurrences(of: "$", with: args) let secureSource = CosmeticFiltersScriptHandler.secureScript( handlerNamesMap: [ @@ -97,7 +102,7 @@ class SiteStateListenerScriptHandler: TabContentScript { } } - @MainActor private func makeArgs(from modelTuples: [CachedAdBlockEngine.CosmeticFilterModelTuple], frameURL: URL) throws -> String { + @MainActor private func makeArgs(from modelTuples: [CachedAdBlockEngine.CosmeticFilterModelTuple], frameURL: URL, isAggressive: Bool) throws -> String { var standardSelectors: Set = [] var agressiveSelectors: Set = [] var styleSelectors: [String: Set] = [:] @@ -126,7 +131,7 @@ class SiteStateListenerScriptHandler: TabContentScript { // (i.e. we don't hide first party content on standard mode) let setup = UserScriptType.SelectorsPollerSetup( frameURL: frameURL, - hideFirstPartyContent: false, + hideFirstPartyContent: isAggressive, genericHide: modelTuples.contains { $0.model.genericHide }, firstSelectorsPollingDelayMs: nil, switchToSelectorsPollingThreshold: 1000, diff --git a/Sources/BraveShields/BraveShield.swift b/Sources/BraveShields/BraveShield.swift index e7a6ebbcb11..8b59a6ad576 100644 --- a/Sources/BraveShields/BraveShield.swift +++ b/Sources/BraveShields/BraveShield.swift @@ -18,7 +18,7 @@ public enum BraveShield { case .AllOff: return false case .AdblockAndTp: - return Preferences.Shields.blockAdsAndTracking.value + return Preferences.Shields.blockAdsAndTrackingLevel.isEnabled case .SafeBrowsing: return Preferences.Shields.blockPhishingAndMalware.value case .FpProtection: diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 705e78fc031..5ab905aec41 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -1036,8 +1036,12 @@ extension Strings { public static let privateBrowsingOnly = NSLocalizedString("PrivateBrowsingOnly", tableName: "BraveShared", bundle: .module, value: "Private Browsing Only", comment: "Setting to keep app in private mode") public static let shieldsDefaults = NSLocalizedString("ShieldsDefaults", tableName: "BraveShared", bundle: .module, value: "Brave Shields Global Defaults", comment: "Section title for adbblock, tracking protection, HTTPS-E, and cookies") public static let shieldsDefaultsFooter = NSLocalizedString("ShieldsDefaultsFooter", tableName: "BraveShared", bundle: .module, value: "These are the default Shields settings for new sites. Changing these won't affect your existing per-site settings.", comment: "Section footer for global shields defaults") - public static let blockAdsAndTracking = NSLocalizedString("BlockAdsAndTracking", tableName: "BraveShared", bundle: .module, value: "Block Cross-Site Trackers", comment: "") - public static let blockAdsAndTrackingDescription = NSLocalizedString("BlockAdsAndTrackingDescription", tableName: "BraveShared", bundle: .module, value: "Prevents ads, popups, and trackers from loading.", comment: "") + public static let trackersAndAdsBlocking = NSLocalizedString("TrackersAndAdsBlocking", tableName: "BraveShared", bundle: .module, value: "Trackers & Ads Blocking", comment: "A label for a shield option that allows you to switch between different blocking levels for tracker and ads blocking. Options include disabled, standard and aggressive.") + public static let trackersAndAdsBlockingDescription = NSLocalizedString("BlockAdsAndTrackingDescription", tableName: "BraveShared", bundle: .module, value: "Prevents ads, popups, and trackers from loading.", comment: "") + public static let trackersAndAdsBlockingDisabled = NSLocalizedString("BlockAdsAndTrackingDisabled", tableName: "BraveShared", bundle: .module, value: "Disabled", comment: "The option the user can select to disable ad and tracker blocking") + public static let trackersAndAdsBlockingAggressive = NSLocalizedString("BlockAdsAndTrackingAggressive", tableName: "BraveShared", bundle: .module, value: "Aggressive", comment: "The option the user can select to do aggressive ad and tracker blocking") + public static let trackersAndAdsBlockingStandard = NSLocalizedString("BlockAdsAndTrackingStandard", tableName: "BraveShared", bundle: .module, value: "Standard", comment: "The option the user can select to do standard (non-aggressive) ad and tracker blocking") + public static let HTTPSEverywhere = NSLocalizedString("HTTPSEverywhere", tableName: "BraveShared", bundle: .module, value: "Upgrade Connections to HTTPS", comment: "") public static let HTTPSEverywhereDescription = NSLocalizedString("HTTPSEverywhereDescription", tableName: "BraveShared", bundle: .module, value: "Opens sites using secure HTTPS instead of HTTP when possible.", comment: "") public static let blockPhishingAndMalware = NSLocalizedString("BlockPhishingAndMalware", tableName: "BraveShared", bundle: .module, value: "Block Phishing and Malware", comment: "") diff --git a/Sources/BraveUI/UIKit/SettingsRowViews.swift b/Sources/BraveUI/UIKit/SettingsRowViews.swift index 8a36f189a50..84a75d70069 100644 --- a/Sources/BraveUI/UIKit/SettingsRowViews.swift +++ b/Sources/BraveUI/UIKit/SettingsRowViews.swift @@ -6,6 +6,9 @@ import Foundation import Static import Preferences import UIKit +import DesignSystem +import SnapKit +import Strings /// The same style switch accessory view as in Static framework, except will not be recreated each time the Cell /// is configured, since it will be stored as is in `Row.Accessory.view` @@ -30,6 +33,74 @@ public class SwitchAccessoryView: UISwitch { } } +public protocol PickerAccessoryViewValue { + var id: String { get } + var localizedTitle: String { get } +} + +public class PickerAccessoryView: UIButton { + public typealias ValueChange = (PickerAccessoryViewValue) -> Void + private let textColor = UIColor.secondaryBraveLabel + + private let options: [PickerAccessoryViewValue] + private let valueChange: ValueChange + + /// The current selected id for the options above + /// Needs to be one of the values available in options + public var selectedValue: PickerAccessoryViewValue { + didSet { + guard oldValue.id != selectedValue.id else { return } + setTitle(selectedValue.localizedTitle, for: .normal) + setMenu() + } + } + + public override var accessibilityValue: String? { + get { return title(for: .normal) } + set { assertionFailure() } // swiftlint:disable:this unused_setter_value + } + + public init(options: [PickerAccessoryViewValue], selectedValue: PickerAccessoryViewValue, valueChange: @escaping ValueChange) { + self.selectedValue = selectedValue + self.options = options + self.valueChange = valueChange + super.init(frame: CGRect(width: 105, height: 40)) + + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: "chevron.up.chevron.down") + configuration.imagePlacement = .trailing + configuration.titleAlignment = .trailing + configuration.contentInsets = .zero + configuration.imagePadding = 4 + configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(pointSize: 10) + configuration.baseForegroundColor = .secondaryBraveLabel + configuration.title = selectedValue.localizedTitle + self.configuration = configuration + showsMenuAsPrimaryAction = true + setMenu() + } + + private func setMenu() { + menu = UIMenu( + title: "", + options: [.displayInline, .singleSelection], + + children: options.map { value in + let state: UIMenuElement.State = value.id == selectedValue.id ? .on : .off + + return UIAction(title: value.localizedTitle, state: state, handler: { [weak self] _ in + self?.selectedValue = value + self?.valueChange(value) + }) + } + ) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + extension Row { /// Creates a switch toggle `Row` which updates a `Preferences.Option` public static func boolRow(title: String, detailText: String? = nil, option: Preferences.Option, onValueChange: SwitchAccessoryView.ValueChange? = nil, image: UIImage? = nil) -> Row { @@ -54,6 +125,35 @@ extension Row { reuseIdentifier: cellReuseId ) } + + /// Creates a switch toggle `Row` which holds local value and no preference update + public static func pickerRow(uuid: UUID = UUID(), title: String, detailText: String?, options: [PickerAccessoryViewValue], selectedValue: PickerAccessoryViewValue, valueChange: @escaping PickerAccessoryView.ValueChange) -> Row { + let pickerView = PickerAccessoryView( + options: options, selectedValue: selectedValue, valueChange: valueChange + ) + + // Get the largest possible size + var frame: CGRect = pickerView.frame + for option in options { + pickerView.selectedValue = option + pickerView.sizeToFit() + + if frame.width < pickerView.frame.width { + frame = pickerView.frame + } + } + pickerView.selectedValue = selectedValue + pickerView.frame = frame + + return Row( + text: title, + detailText: detailText, + accessory: .view(pickerView), + cellClass: MultilineSubtitleCell.self, + uuid: uuid.uuidString, + reuseIdentifier: "picker_row" + ) + } } public class MultilineButtonCell: ButtonCell { diff --git a/Sources/Data/models/Domain.swift b/Sources/Data/models/Domain.swift index 715383e3164..ca49df728fb 100644 --- a/Sources/Data/models/Domain.swift +++ b/Sources/Data/models/Domain.swift @@ -17,7 +17,8 @@ public final class Domain: NSManagedObject, CRUD { @NSManaged public var blockedFromTopSites: Bool // don't show ever on top sites @NSManaged public var shield_allOff: NSNumber? - @NSManaged public var shield_adblockAndTp: NSNumber? + @NSManaged private var shield_adblockAndTp: NSNumber? + @NSManaged private var shield_adblockAndTpAggressive: NSNumber @available(*, deprecated, message: "Per domain HTTPSE shield is currently unused.") @NSManaged public var shield_httpse: NSNumber? @@ -39,6 +40,28 @@ public final class Domain: NSManagedObject, CRUD { private static let containsEthereumPermissionsPredicate = NSPredicate(format: "wallet_permittedAccounts != nil && wallet_permittedAccounts != ''") private static let containsSolanaPermissionsPredicate = NSPredicate(format: "wallet_solanaPermittedAcccounts != nil && wallet_solanaPermittedAcccounts != ''") + @MainActor public var adBlockAndTPShieldLevel: Preferences.Shields.ShieldLevel { + get { + guard let shield_adblockAndTp = shield_adblockAndTp else { + // If this value is nil, revert to global preferences + return Preferences.Shields.blockAdsAndTrackingLevel + } + + if !shield_adblockAndTp.boolValue { + return .disabled + } else if shield_adblockAndTpAggressive == true { + return .aggressive + } else { + return .standard + } + } + + set { + shield_adblockAndTp = newValue.isEnabled as NSNumber + shield_adblockAndTpAggressive = newValue.isAggressive as NSNumber + } + } + @MainActor public var areAllShieldsOff: Bool { return shield_allOff?.boolValue ?? false } @@ -98,8 +121,24 @@ public final class Domain: NSManagedObject, CRUD { forUrl url: URL, shield: BraveShield, isOn: Bool?, isPrivateBrowsing: Bool ) { - let _context: WriteContext = isPrivateBrowsing ? .new(inMemory: true) : .new(inMemory: false) - setBraveShieldInternal(forUrl: url, shield: shield, isOn: isOn, context: _context) + setBraveShieldInternal( + forUrl: url, + shield: shield, + isOn: isOn, + context: writeContext(isPrivateBrowsing: isPrivateBrowsing) + ) + } + + @MainActor public class func setAdAndTP(shieldLevel: Preferences.Shields.ShieldLevel, for url: URL, isPrivateBrowsing: Bool) { + setAdAndTPInternal( + shieldLevel: shieldLevel, + for: url, + context: writeContext(isPrivateBrowsing: isPrivateBrowsing) + ) + } + + private class func writeContext(isPrivateBrowsing: Bool) -> WriteContext { + return isPrivateBrowsing ? .new(inMemory: true) : .new(inMemory: false) } /// Whether or not a given shield should be enabled based on domain exceptions and the users global preference @@ -109,7 +148,7 @@ public final class Domain: NSManagedObject, CRUD { case .AllOff: return self.shield_allOff?.boolValue ?? false case .AdblockAndTp: - return self.shield_adblockAndTp?.boolValue ?? Preferences.Shields.blockAdsAndTracking.value + return adBlockAndTPShieldLevel.isEnabled case .SafeBrowsing: return self.shield_safeBrowsing?.boolValue ?? Preferences.Shields.blockPhishingAndMalware.value case .FpProtection: @@ -158,7 +197,7 @@ public final class Domain: NSManagedObject, CRUD { } public class func totalDomainsWithAdblockShieldsLoweredFromGlobal() -> Int { - guard Preferences.Shields.blockAdsAndTracking.value, + guard Preferences.Shields.blockAdsAndTrackingLevel.isEnabled, let domains = Domain.all(where: NSPredicate(format: "shield_adblockAndTp != nil")) else { return 0 // Can't be lower than off } @@ -166,7 +205,7 @@ public final class Domain: NSManagedObject, CRUD { } public class func totalDomainsWithAdblockShieldsIncreasedFromGlobal() -> Int { - guard !Preferences.Shields.blockAdsAndTracking.value, + guard !Preferences.Shields.blockAdsAndTrackingLevel.isEnabled, let domains = Domain.all(where: NSPredicate(format: "shield_adblockAndTp != nil")) else { return 0 // Can't be higher than on } @@ -389,15 +428,30 @@ extension Domain { let domain = Domain.getOrCreateInternal( url, context: context, saveStrategy: .delayedPersistentStore) - domain.setBraveShield(shield: shield, isOn: isOn, context: context) + domain.setBraveShield(shield: shield, isOn: isOn) + } + } + + @MainActor class func setAdAndTPInternal(shieldLevel: Preferences.Shields.ShieldLevel, for url: URL, context: WriteContext) { + DataController.perform(context: context) { context in + // Not saving here, save happens in `perform` method. + let domain = Domain.getOrCreateInternal( + url, context: context, + saveStrategy: .delayedPersistentStore) + domain.setAdAndTP(shieldLevel: shieldLevel) } } + + @MainActor private func setAdAndTP( + shieldLevel: Preferences.Shields.ShieldLevel + ) { + shield_adblockAndTp = shieldLevel.isEnabled as NSNumber + shield_adblockAndTpAggressive = shieldLevel.isAggressive as NSNumber + } private func setBraveShield( - shield: BraveShield, isOn: Bool?, - context: NSManagedObjectContext + shield: BraveShield, isOn: Bool? ) { - let setting = (isOn == shield.globalPreference ? nil : isOn) as NSNumber? switch shield { case .AllOff: shield_allOff = setting @@ -408,22 +462,6 @@ extension Domain { } } - /// Get whether or not a shield override is set for a given shield. - private func getBraveShield(_ shield: BraveShield) -> Bool? { - switch shield { - case .AllOff: - return self.shield_allOff?.boolValue - case .AdblockAndTp: - return self.shield_adblockAndTp?.boolValue - case .SafeBrowsing: - return self.shield_safeBrowsing?.boolValue - case .FpProtection: - return self.shield_fpProtection?.boolValue - case .NoScript: - return self.shield_noScript?.boolValue - } - } - /// Returns `url` but switches the scheme from `http` <-> `https` private func domainForInverseHttpScheme(context: NSManagedObjectContext) -> Domain? { diff --git a/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion b/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion index 2f2f85611e5..5d29381a932 100644 --- a/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion +++ b/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model20.xcdatamodel + Model21.xcdatamodel diff --git a/Sources/Data/models/Model.xcdatamodeld/Model21.xcdatamodel/contents b/Sources/Data/models/Model.xcdatamodeld/Model21.xcdatamodel/contents new file mode 100644 index 00000000000..fdb712fdf53 --- /dev/null +++ b/Sources/Data/models/Model.xcdatamodeld/Model21.xcdatamodel/contents @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/Preferences/GlobalPreferences.swift b/Sources/Preferences/GlobalPreferences.swift index b456683a3dd..787646f0120 100644 --- a/Sources/Preferences/GlobalPreferences.swift +++ b/Sources/Preferences/GlobalPreferences.swift @@ -34,10 +34,82 @@ extension Preferences { } public final class Shields { + /// A 3 part option for shield levels varying in strength of blocking content + public enum ShieldLevel: String, CaseIterable { + // Q46 What is the global ad blocking shields setting? + public enum P3AAnswer: Int, CaseIterable { + case disabled = 0 + case standard = 1 + case aggressive = 2 + } + + /// Mode blocks all content + case aggressive + /// Mode indicating that 1st party content is not blocked for default and regional lists + case standard + /// Mode indicating this setting is disabled + case disabled + + /// Wether this setting indicates that the shields are enabled or not + public var isEnabled: Bool { + switch self { + case .aggressive, .standard: return true + case .disabled: return false + } + } + + /// Wether this setting indicates that the shields are enabled or not + public var isAggressive: Bool { + switch self { + case .aggressive: return true + case .disabled, .standard: return false + } + } + + /// Return the P3A answer associated with this enum + public var p3AAnswer: P3AAnswer { + switch self { + case .disabled: return .disabled + case .standard: return .standard + case .aggressive: return .aggressive + } + } + } + public static let allShields = [blockAdsAndTracking, httpsEverywhere, blockPhishingAndMalware, googleSafeBrowsing, blockScripts, fingerprintingProtection, blockImages] /// Shields will block ads and tracking if enabled public static let blockAdsAndTracking = Option(key: "shields.block-ads-and-tracking", default: true) + /// Shields will block ads and tracking aggressive mode is enabled + public static let blockAdsAndTrackingAggressive = Option(key: "shields.block-ads-and-tracking-aggressive", default: false) + + /// Get the level of the adblock and tracking protection + public static var blockAdsAndTrackingLevel: ShieldLevel { + get { + if !blockAdsAndTracking.value { + return .disabled + } else if blockAdsAndTrackingAggressive.value { + return .aggressive + } else { + return .standard + } + } + + set { + switch newValue { + case .aggressive: + blockAdsAndTrackingAggressive.value = true + blockAdsAndTracking.value = true + case .disabled: + blockAdsAndTrackingAggressive.value = false + blockAdsAndTracking.value = false + case .standard: + blockAdsAndTrackingAggressive.value = false + blockAdsAndTracking.value = true + } + } + } + /// Websites will be upgraded to HTTPS if a loaded page attempts to use HTTP public static let httpsEverywhere = Option(key: "shields.https-everywhere", default: true) /// Enable Google Safe Browsing