From b0477f9b926f31156c4fa37b826c541c60e2d44f Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Tue, 7 May 2024 10:40:12 +0100 Subject: [PATCH] Add custom text filter list --- .../Browser/Helpers/LaunchHelper.swift | 4 +- .../DefaultShieldsSectionView.swift | 4 +- .../FilterLists/CustomFilterListView.swift | 264 ++++++++++++++++++ .../FilterLists/FilterListAddURLView.swift | 17 +- .../FilterLists/FilterListsView.swift | 77 ++++- .../OtherPrivacySettingsSectionView.swift | 2 +- .../AdBlock/AdBlockEngineManager.swift | 4 +- .../AdBlock/AdBlockGroupsManager.swift | 84 +++++- .../AdBlock/GroupedAdBlockEngine.swift | 4 +- .../ContentBlockerManager.swift | 83 +++++- .../WebFilters/CustomFilterListStorage.swift | 135 +++++++++ .../Sources/BraveShields/ShieldStrings.swift | 204 ++++++++++++++ .../Sources/BraveStrings/BraveStrings.swift | 192 +++---------- .../Extensions/GeometryExtensions.swift | 9 + .../AdBlockGroupsManagerTests.swift | 7 - .../ContentBlockerManagerTests.swift | 46 ++- 16 files changed, 930 insertions(+), 206 deletions(-) create mode 100644 ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/CustomFilterListView.swift diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/LaunchHelper.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/LaunchHelper.swift index 884f41f85b437..7334938b5d75d 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/LaunchHelper.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Helpers/LaunchHelper.swift @@ -130,9 +130,9 @@ public actor LaunchHelper { // All custom filter list urls .union( CustomFilterListStorage.shared.filterListsURLs.map { - .customFilterList(uuid: $0.setting.uuid) + .filterListURL(uuid: $0.setting.uuid) } - ) + ).union([.filterListText]) } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/DefaultShieldsSectionView.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/DefaultShieldsSectionView.swift index 8604f0e5460ad..4771a32374ab5 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/DefaultShieldsSectionView.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/DefaultShieldsSectionView.swift @@ -128,8 +128,8 @@ struct DefaultShieldsViewView: View { FilterListsView() } label: { LabelView( - title: Strings.contentFiltering, - subtitle: Strings.contentFilteringDescription + title: Strings.Shields.contentFiltering, + subtitle: Strings.Shields.contentFilteringDescription ) }.listRowBackground(Color(.secondaryBraveGroupedBackground)) } header: { diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/CustomFilterListView.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/CustomFilterListView.swift new file mode 100644 index 0000000000000..9bd6cbe077428 --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/CustomFilterListView.swift @@ -0,0 +1,264 @@ +// 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 BraveUI +import DesignSystem +import Strings +import SwiftUI + +struct CustomFilterListView: View { + @Environment(\.dismiss) private var dismiss: DismissAction + @ObservedObject private var customFilterListStorage = CustomFilterListStorage.shared + /// The passed custom rules which we will be editing + @Binding private var customRules: String? + /// Any errors that are seen during saving or editing which we can display + @State private var rulesError: Error? + /// A state for showing/hiding the cancelation alert + @State private var showCancelAlert = false + /// Tells us if our text content is empty which allows us to show the prompt text + @State private var isTextEmpty = false + /// Our coordinator manages the content of the input text and gives us information back + /// when saving up cancelling + private var coordinator: FilterListEditor.Coordinator + + /// Tells us if we have changes from the original text + private var hasChanges: Bool { + return coordinator.text != customRules ?? "" + } + + /// The shape of our text input box area + private var borderShape: some InsettableShape { + RoundedRectangle(cornerRadius: 12, style: .continuous) + } + + init(customRules: Binding) { + _customRules = customRules + isTextEmpty = customRules.wrappedValue?.isEmpty ?? true + coordinator = FilterListEditor.Coordinator() + coordinator.text = customRules.wrappedValue ?? "" + } + + var body: some View { + NavigationView { + VStack(alignment: .leading) { + FilterListEditor( + coordinator: coordinator, + error: $rulesError, + isTextEmpty: $isTextEmpty + ) + .overlay( + Text(Strings.Shields.customFiltersPlaceholder) + .multilineTextAlignment(.leading) + .padding(.vertical, 14) + .padding(.horizontal, 16) + .disabled(true) + .allowsHitTesting(false) + .font(.body) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .topLeading + ) + .foregroundColor(Color(.placeholderText)) + .opacity(isTextEmpty ? 1 : 0) + .accessibilityHidden(!isTextEmpty), + alignment: .topLeading + ) + .clipShape(borderShape) + + if let error = rulesError { + SectionFooterErrorView(errorMessage: error.localizedDescription) + .padding(.horizontal, 12) + .padding(.bottom, 0) + } + } + .padding() + .osAvailabilityModifiers({ view in + if #available(iOS 16.4, *) { + view + .scrollContentBackground(.hidden) + .scrollDismissesKeyboard(.interactively) + } else { + view.introspectTextView { textView in + textView.backgroundColor = .clear + } + } + }) + .background( + Color(.secondaryBraveBackground) + .edgesIgnoringSafeArea(.all) + ) + .navigationTitle(Text(Strings.Shields.customFilters)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button( + action: { + if hasChanges { + showCancelAlert = true + } else { + dismiss() + } + }, + label: { + Text(Strings.CancelString) + } + ) + .alert( + isPresented: $showCancelAlert, + content: { + return Alert( + title: Text(Strings.dismissChangesConfirmationTitle), + message: Text(Strings.dismissChangesConfirmationMessage), + primaryButton: .destructive( + Text(Strings.dismissChangesButtonTitle), + action: { + dismiss() + } + ), + secondaryButton: .cancel( + Text(Strings.cancelButtonTitle) + ) + ) + } + ) + } + + ToolbarItem(placement: .confirmationAction) { + Button( + action: saveCustomRules, + label: { + Label(Strings.saveButtonTitle, braveSystemImage: "leo.check.normal") + .labelStyle(.titleOnly) + } + ) + } + } + } + } + + private func saveCustomRules() { + let pendingRules = coordinator.text + + Task { + do { + if !pendingRules.isEmpty { + try await customFilterListStorage.save(customRules: pendingRules) + customRules = pendingRules + } else { + try await customFilterListStorage.deleteCustomRules() + customRules = nil + } + + dismiss() + } catch { + // Could not load the rules + self.rulesError = error + } + } + } +} + +#Preview { + CustomFilterListView( + customRules: .constant( + """ + ! Hide the header on example.com (CF Test) + example.com,example.net##h1 + + ! Hide the brave logo on brave.com (Network test) + ||brave.com/static-assets/images/brave-logo-sans-text.svg + """ + ) + ) +} + +/// An editor for custom filter lists which limits the number of lines entered +/// +/// - Note: We don't pass a binding text value +/// as this will cause glitches when editing. +/// Instead we pass bindings for individual states, such as `isTextEmpty` +/// Later we can pull the updated text directly from the coordinator when saving or cancelling. +struct FilterListEditor: UIViewRepresentable { + /// The coordinator to use when editing + let coordinator: Coordinator + /// Any errors that might occur during editing + @Binding var error: Error? + /// This is updated everytime the emptiness of the text changes + /// so we can add things like placeholders + @Binding var isTextEmpty: Bool + + func makeCoordinator() -> Coordinator { + return coordinator + } + + func makeUIView(context: Context) -> UITextView { + context.coordinator.textView + } + + func updateUIView(_ uiView: UITextView, context: Context) { + context.coordinator.errorEditingText = { error in + self.error = error + } + context.coordinator.stringDidChange = { string in + let isEmpty = string.isEmpty + guard isEmpty != isTextEmpty else { return } + isTextEmpty = isEmpty + } + } + + class Coordinator: NSObject, UITextViewDelegate { + lazy var textView: UITextView = { + let textView = UITextView() + textView.font = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) + textView.textColor = UIColor.braveLabel + textView.textContainerInset = UIEdgeInsets( + vertical: 16, + horizontal: 12 + ) + textView.delegate = self + return textView + }() + + var stringDidChange: ((String) -> Void)? + var errorEditingText: ((Error) -> Void)? + + var text: String { + get { + return textView.text + } + set { + textView.text = newValue + } + } + + func textViewDidChange(_ textView: UITextView) { + stringDidChange?(textView.text) + } + + func textView( + _ textView: UITextView, + shouldChangeTextIn range: NSRange, + replacementText text: String + ) -> Bool { + let currentText = textView.text ?? "" + guard let stringRange = Range(range, in: currentText) else { return false } + let updatedText = currentText.replacingCharacters(in: stringRange, with: text) + let lines = updatedText.components(separatedBy: .newlines) + + guard lines.count <= CustomFilterListStorage.maxNumberOfCustomRulesLines else { + errorEditingText?( + CustomFilterListStorage.CustomRulesError.tooManyLines( + max: CustomFilterListStorage.maxNumberOfCustomRulesLines + ) + ) + return false + } + + return true + } + } +} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListAddURLView.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListAddURLView.swift index d3160539d7f1f..6dfb42a51c607 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListAddURLView.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListAddURLView.swift @@ -3,6 +3,7 @@ // 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 BraveUI import DesignSystem import Strings @@ -16,7 +17,7 @@ struct FilterListAddURLView: View { @FocusState private var isURLFieldFocused: Bool private var textField: some View { - TextField(Strings.filterListsEnterFilterListURL, text: $newURLInput) + TextField(Strings.Shields.filterListsEnterFilterListURL, text: $newURLInput) .onChange(of: newURLInput) { newValue in errorMessage = nil } @@ -41,16 +42,16 @@ struct FilterListAddURLView: View { }.listRowBackground(Color(.secondaryBraveGroupedBackground)) }, header: { - Text(Strings.customFilterListURL) + Text(Strings.Shields.customFilterListURL) }, footer: { VStack(alignment: .leading, spacing: 0) { SectionFooterErrorView(errorMessage: errorMessage) VStack(alignment: .leading, spacing: 8) { - Text(Strings.addCustomFilterListDescription) + Text(Strings.Shields.addCustomFilterListDescription) .fixedSize(horizontal: false, vertical: true) - Text(LocalizedStringKey(Strings.addCustomFilterListWarning)) + Text(LocalizedStringKey(Strings.Shields.addCustomFilterListWarning)) .fixedSize(horizontal: false, vertical: true) }.padding(.top) } @@ -60,11 +61,11 @@ struct FilterListAddURLView: View { .animation(.easeInOut, value: errorMessage) .listBackgroundColor(Color(UIColor.braveGroupedBackground)) .listStyle(.insetGrouped) - .navigationTitle(Strings.customFilterList) + .navigationTitle(Strings.Shields.customFilterList) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .confirmationAction) { - Button(Strings.filterListsAdd) { + Button(Strings.Shields.filterListsAdd) { handleOnSubmit() }.disabled(newURLInput.isEmpty) } @@ -84,11 +85,11 @@ struct FilterListAddURLView: View { private func handleOnSubmit() { guard !newURLInput.isEmpty else { return } guard let url = URL(string: newURLInput) else { - self.errorMessage = Strings.filterListAddInvalidURLError + self.errorMessage = Strings.Shields.filterListAddInvalidURLError return } guard url.scheme == "https" else { - self.errorMessage = Strings.filterListAddOnlyHTTPSAllowedError + self.errorMessage = Strings.Shields.filterListAddOnlyHTTPSAllowedError return } guard diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListsView.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListsView.swift index fc366c4f89153..fd6d295711aaa 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListsView.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/FilterLists/FilterListsView.swift @@ -4,6 +4,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import BraveCore +import BraveShields import BraveUI import Data import DesignSystem @@ -12,11 +13,15 @@ import SwiftUI /// A view showing enabled and disabled community filter lists struct FilterListsView: View { + private static let dateFormatter = RelativeDateTimeFormatter() + @ObservedObject private var filterListStorage = FilterListStorage.shared @ObservedObject private var customFilterListStorage = CustomFilterListStorage.shared @Environment(\.editMode) private var editMode @State private var showingAddSheet = false - private let dateFormatter = RelativeDateTimeFormatter() + @State private var showingCustomFiltersSheet = false + @State private var customRules: String? + @State private var rulesError: Error? var body: some View { List { @@ -26,7 +31,7 @@ struct FilterListsView: View { Button { showingAddSheet = true } label: { - Text(Strings.addCustomFilterList) + Text(Strings.Shields.addCustomFilterList) .foregroundColor(Color(.braveBlurpleTint)) } .disabled(editMode?.wrappedValue.isEditing == true) @@ -37,7 +42,7 @@ struct FilterListsView: View { } ) } header: { - Text(Strings.customFilterLists) + Text(Strings.Shields.customFilterLists) } .listRowBackground(Color(.secondaryBraveGroupedBackground)) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -46,23 +51,64 @@ struct FilterListsView: View { filterListView } header: { VStack(alignment: .leading, spacing: 4) { - Text(Strings.defaultFilterLists) + Text(Strings.Shields.defaultFilterLists) .textCase(.uppercase) - Text(Strings.filterListsDescription) + Text(Strings.Shields.filterListsDescription) .textCase(.none) } }.listRowBackground(Color(.secondaryBraveGroupedBackground)) + + Section { + Button { + showingCustomFiltersSheet = true + } label: { + if let customRules = customRules { + VStack(alignment: .leading) { + Text(customRules) + .lineLimit(2) + .multilineTextAlignment(.leading) + .foregroundStyle(Color(.braveLabel)) + .font(.system(size: 14, weight: .regular, design: .monospaced)) + } + } else if let error = rulesError { + Text(error.localizedDescription) + .foregroundStyle(Color(.braveErrorLabel)) + .font(.subheadline) + } else { + Text(Strings.Shields.customFiltersPlaceholder) + .foregroundStyle(Color(.secondaryBraveLabel)) + .font(.subheadline) + } + } + } header: { + VStack(alignment: .leading, spacing: 4) { + Text(Strings.Shields.customFilters) + .textCase(.uppercase) + Text(Strings.Shields.customFiltersDescription) + .textCase(.none) + } + } } + .popover( + isPresented: $showingCustomFiltersSheet, + content: { + CustomFilterListView(customRules: $customRules) + .interactiveDismissDisabled() + } + ) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .animation(.default, value: customFilterListStorage.filterListsURLs) .listBackgroundColor(Color(UIColor.braveGroupedBackground)) .listStyle(.insetGrouped) - .navigationTitle(Strings.contentFiltering) + .navigationTitle(Strings.Shields.contentFiltering) .toolbar { EditButton().disabled( customFilterListStorage.filterListsURLs.isEmpty && editMode?.wrappedValue.isEditing == false ) } + .onAppear(perform: { + loadCustomRules() + }) } @ViewBuilder private var filterListView: some View { @@ -112,18 +158,18 @@ struct FilterListsView: View { case .downloaded(let downloadDate): Text( String.localizedStringWithFormat( - Strings.filterListsLastUpdated, - dateFormatter.localizedString(for: downloadDate, relativeTo: Date()) + Strings.Shields.filterListsLastUpdated, + Self.dateFormatter.localizedString(for: downloadDate, relativeTo: Date()) ) ) .font(.caption) .foregroundColor(Color(.braveLabel)) case .failure: - Text(Strings.filterListsDownloadFailed) + Text(Strings.Shields.filterListsDownloadFailed) .font(.caption) .foregroundColor(.red) case .pending: - Text(Strings.filterListsDownloadPending) + Text(Strings.Shields.filterListsDownloadPending) .font(.caption) .foregroundColor(Color(.braveLabel)) } @@ -161,7 +207,7 @@ struct FilterListsView: View { // during the `cleaupInvalidRuleLists` step on `LaunchHelper` // 2. Stop downloading the file - await FilterListCustomURLDownloader.shared.stopFetching( + FilterListCustomURLDownloader.shared.stopFetching( filterListCustomURL: removedURL ) @@ -181,6 +227,15 @@ struct FilterListsView: View { } } } + + private func loadCustomRules() { + do { + self.customRules = try customFilterListStorage.loadCustomRules() + } catch { + rulesError = error + customRules = nil + } + } } #if DEBUG diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/OtherPrivacySettingsSectionView.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/OtherPrivacySettingsSectionView.swift index f95259efbad9e..a7944766f9e40 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/OtherPrivacySettingsSectionView.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/Features/ShieldsPrivacy/OtherPrivacySettingsSectionView.swift @@ -65,7 +65,7 @@ struct OtherPrivacySettingsSectionView: View { } ) ToggleView( - title: Strings.blockMobileAnnoyances, + title: Strings.Shields.blockMobileAnnoyances, subtitle: nil, toggle: $settings.blockMobileAnnoyances ) diff --git a/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockEngineManager.swift b/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockEngineManager.swift index a9ec93c4ffb78..c2542a76537b2 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockEngineManager.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockEngineManager.swift @@ -382,7 +382,9 @@ extension GroupedAdBlockEngine.Source { return .filterList(componentId: componentId, isAlwaysAggressive: isAlwaysAggressive) case .filterListURL(let uuid): - return .customFilterList(uuid: uuid) + return .filterListURL(uuid: uuid) + case .filterListText: + return .filterListText } } } diff --git a/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockGroupsManager.swift b/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockGroupsManager.swift index 1677f5cfd62d9..f2b55bc841df2 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockGroupsManager.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/AdBlockGroupsManager.swift @@ -165,6 +165,52 @@ import os } } + /// Update the file managers with the latest files and will compile the engines right away. + /// - Parameters: + /// - fileInfos: The file infos to update on the appropriate engine manager + func updateImmediately( + fileInfos: [AdBlockEngineManager.FileInfo] + ) async { + let enabledSources = sourceProvider.enabledSources + + for engineType in GroupedAdBlockEngine.EngineType.allCases { + let manager = getManager(for: engineType) + let sources = sourceProvider.sources(for: engineType) + var updatedFiles = false + + // Compile content blockers if this filter list is enabled + for fileInfo in fileInfos { + guard sources.contains(fileInfo.filterListInfo.source) else { + // This file is not for this engine type + continue + } + + if enabledSources.contains(fileInfo.filterListInfo.source) { + await ensureContentBlockers(for: fileInfo, engineType: engineType) + } + + updatedFiles = true + manager.add(fileInfo: fileInfo) + } + + if updatedFiles { + await manager.compileImmediatelyIfNeeded( + for: enabledSources, + resourcesInfo: resourcesInfo + ) + } + } + } + + /// Handle updated filter list info. Will compile the engine immediately. + /// - Parameters: + /// - fileInfo: The file info to update on the appropriate engine manager + func updateImmediately( + fileInfo: AdBlockEngineManager.FileInfo + ) async { + await updateImmediately(fileInfos: [fileInfo]) + } + /// Handle updated filter list info /// - Parameters: /// - fileInfo: The file info to update on the appropriate engine manager @@ -181,6 +227,13 @@ import os removeFileInfos(for: [source]) } + /// Remove the file info from the list that is no longer available and compile the engines if it is needed. + func removeFileInfoImmediately( + for source: GroupedAdBlockEngine.Source + ) async { + await removeFileInfosImmediately(for: [source]) + } + /// Remove the file infos from the list that is no longer available and compile the engines if it is needed. func removeFileInfos( for sources: [GroupedAdBlockEngine.Source] @@ -198,6 +251,23 @@ import os } } + /// Remove the file infos from the list that is no longer available and compile the engines if it is needed. + func removeFileInfosImmediately( + for sources: [GroupedAdBlockEngine.Source] + ) async { + for engineType in GroupedAdBlockEngine.EngineType.allCases { + let manager = getManager(for: engineType) + for source in sources { + manager.removeInfo(for: source) + } + + await manager.compileImmediatelyIfNeeded( + for: sourceProvider.enabledSources, + resourcesInfo: resourcesInfo + ) + } + } + /// Immediately compile any engines that have all the files ready. /// Will not compile anything is there is already the same set of files being compiled. func compileEnginesIfFilesAreReady() { @@ -209,10 +279,8 @@ import os /// Immediately compile the engine for the given type if it has all the files ready.. /// Will not compile anything is there is already the same set of files being compiled. func compileEngineIfFilesAreReady(for engineType: GroupedAdBlockEngine.EngineType) { - let allEnabledSources = sourceProvider.enabledSources - let engineTypeSources = sourceProvider.sources(for: engineType) - let enabledSources = allEnabledSources.filter({ engineTypeSources.contains($0) }) let manager = self.getManager(for: engineType) + let enabledSources = sourceProvider.enabledSources(for: engineType) guard manager.checkHasAllInfo(for: enabledSources) else { return } Task { @@ -467,6 +535,7 @@ extension AdBlockEngineManager.FileInfo { var enabledSources: [GroupedAdBlockEngine.Source] { var enabledSources = FilterListStorage.shared.enabledSources enabledSources.append(contentsOf: CustomFilterListStorage.shared.enabledSources) + enabledSources.append(contentsOf: [.filterListText]) return enabledSources } @@ -475,13 +544,14 @@ extension AdBlockEngineManager.FileInfo { func sources( for engineType: GroupedAdBlockEngine.EngineType ) -> [GroupedAdBlockEngine.Source] { - var enabledSources = FilterListStorage.shared.sources(for: engineType) switch engineType { case .aggressive: - enabledSources.append(contentsOf: CustomFilterListStorage.shared.enabledSources) - return enabledSources + var sources = FilterListStorage.shared.sources(for: engineType) + sources.append(contentsOf: CustomFilterListStorage.shared.allSources) + sources.append(contentsOf: [.filterListText]) + return sources case .standard: - return enabledSources + return FilterListStorage.shared.sources(for: engineType) } } diff --git a/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/GroupedAdBlockEngine.swift b/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/GroupedAdBlockEngine.swift index fca4404dcb4cc..41fa63bc11e80 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/GroupedAdBlockEngine.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/AdBlock/GroupedAdBlockEngine.swift @@ -15,11 +15,13 @@ public actor GroupedAdBlockEngine { public enum Source: Codable, Hashable, CustomDebugStringConvertible { case filterList(componentId: String, uuid: String) case filterListURL(uuid: String) + case filterListText public var debugDescription: String { switch self { case .filterList(let componentId, _): return componentId case .filterListURL(let uuid): return uuid + case .filterListText: return "filter-list-text" } } } @@ -45,7 +47,7 @@ public actor GroupedAdBlockEngine { case .aggressive: return true } } - + public var debugDescription: String { switch self { case .aggressive: return "aggressive" diff --git a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift index f264e6cb45af0..ba9aa2c9fe7eb 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/ContentBlocker/ContentBlockerManager.swift @@ -101,7 +101,8 @@ import os.log case generic(GenericBlocklistType) case filterList(componentId: String, isAlwaysAggressive: Bool) - case customFilterList(uuid: String) + case filterListURL(uuid: String) + case filterListText private var identifier: String { switch self { @@ -109,14 +110,16 @@ import os.log return [Self.genericPrifix, type.bundledFileName].joined(separator: "-") case .filterList(let componentId, _): return [Self.filterListPrefix, componentId].joined(separator: "-") - case .customFilterList(let uuid): + case .filterListURL(let uuid): return [Self.filterListURLPrefix, uuid].joined(separator: "-") + case .filterListText: + return "filter-list-text" } } func mode(isAggressiveMode: Bool) -> BlockingMode { switch self { - case .customFilterList: + case .filterListURL, .filterListText: return .general case .filterList(_, let isAlwaysAggressive): if isAlwaysAggressive || isAggressiveMode { @@ -206,6 +209,75 @@ import os.log Self.signpost.endInterval("cleaupInvalidRuleLists", state) } + /// Test the rules and find the broken rules, their line numbers and associated errors + public func testRules( + forFilterSet filterSet: String + ) async -> (rule: String, line: Int, error: Error)? { + let rules = filterSet.components(separatedBy: .newlines) + return await testRulesBinarySearch(rules: rules, range: 0.. + ) async -> (rule: String, line: Int, error: Error)? { + let rangedRules = rules[range] + guard rangedRules.count > 0 else { return nil } + + do { + // 1. Test engine (we don't care about the results) + _ = try AdblockEngine(rules: rangedRules.joined(separator: "\n")) + + // 2. Test content blockers + let results = try AdblockEngine.contentBlockerRules( + fromFilterSet: rangedRules.joined(separator: "\n") + ) + + let decodedRuleList = try decode(encodedContentRuleList: results.rulesJSON) + + if decodedRuleList.count > 0 { + try await ruleStore.compileContentRuleList( + forIdentifier: "test-identifier", + encodedContentRuleList: results.rulesJSON + ) + + try await ruleStore.removeContentRuleList(forIdentifier: "test-identifier") + } + + return nil + } catch { + if rangedRules.count == 1 { + // Found the culprit line + return (rules[range.lowerBound], range.lowerBound, error) + } + + let middle = range.count / 2 + range.lowerBound + + // Test left + if middle > range.lowerBound, + let failure = await testRulesBinarySearch( + rules: rules, + range: range.lowerBound.. BlocklistType? in guard customURL.setting.isEnabled else { return nil } - return .customFilterList(uuid: customURL.setting.uuid) + return .filterListURL(uuid: customURL.setting.uuid) } - return Set(genericRuleLists).union(additionalRuleLists).union(customRuleLists) + return Set(genericRuleLists).union(additionalRuleLists) + .union(customRuleLists).union([.filterListText]) } /// Return the enabled rule types for this domain and the enabled settings. diff --git a/ios/brave-ios/Sources/Brave/WebFilters/CustomFilterListStorage.swift b/ios/brave-ios/Sources/Brave/WebFilters/CustomFilterListStorage.swift index 47d6fe5bb23b7..74d76a2d44967 100644 --- a/ios/brave-ios/Sources/Brave/WebFilters/CustomFilterListStorage.swift +++ b/ios/brave-ios/Sources/Brave/WebFilters/CustomFilterListStorage.swift @@ -3,11 +3,50 @@ // 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 BraveStrings import Data import Foundation +import Preferences +import WebKit @MainActor class CustomFilterListStorage: ObservableObject { + enum CustomRulesError: Error, LocalizedError { + /// We limit the number of lines because UITextView cannot handle large text + case tooManyLines(max: Int) + /// A rule that failed, the line the rule is found on and the underlying error + /// - Note: The error is usually not that descriptive so generally not too useful. + case failedRule(rule: String, line: Int, error: Error) + + var errorDescription: String? { + switch self { + case .tooManyLines(let max): + return String.localizedStringWithFormat( + Strings.Shields.customFiltersTooManyLinesError, + max + ) + case .failedRule(let rule, let line, _): + return String.localizedStringWithFormat( + Strings.Shields.customFiltersInvalidRuleError, + rule, + line + ) + } + } + } + static let shared = CustomFilterListStorage(persistChanges: true) + /// Max number of lines our custom filter list can be + /// + /// We have a max number of lines for adblock rules because too many lines will kill the editor. + static let maxNumberOfCustomRulesLines = 10_000 + + /// Store the current version of the custom rules so we know if we should upgrade it + private static var customRuleListVersion = Preferences.Option( + key: "shields.custom-rule-list-version", + default: 0 + ) + /// Wether or not to store the data into disk or into memory let persistChanges: Bool /// A list of filter list URLs and their enabled statuses @@ -31,6 +70,17 @@ import Foundation return FilterListCustomURL(setting: setting, downloadStatus: .pending) } } + + do { + // Load the custom text filter list + if let fileInfo = try savedCustomRulesFileInfo() { + AdBlockGroupsManager.shared.update(fileInfo: fileInfo) + } + } catch { + ContentBlockerManager.log.error( + "Failed to load custom filter list: \(String(describing: error))" + ) + } } /// Ensures that the settings for a filter list are stored @@ -44,6 +94,91 @@ import Foundation } } + /// Get the file URL to the custom filter list rules regardless if it is saved or not + private func customRulesFileURL() throws -> URL { + let folderURL = try getOrCreateCustomRulesFolder() + return folderURL.appendingPathComponent("list.txt") + } + + /// Get the file URL to the custom filter list rules if it exists + public func savedCustomRulesFileURL() throws -> URL? { + let fileURL = try customRulesFileURL() + + if FileManager.default.fileExists(atPath: fileURL.path) { + return fileURL + } else { + return nil + } + } + + public func savedCustomRulesFileInfo() throws -> AdBlockEngineManager.FileInfo? { + guard let fileURL = try savedCustomRulesFileURL() else { return nil } + let versionNumber = Self.customRuleListVersion.value + + return AdBlockEngineManager.FileInfo( + filterListInfo: GroupedAdBlockEngine.FilterListInfo( + source: .filterListText, + version: "\(versionNumber)" + ), + localFileURL: fileURL + ) + } + + /// Load the custom filter list rules if they are saved + public func loadCustomRules() throws -> String? { + guard let url = try savedCustomRulesFileURL() else { return nil } + return try String(contentsOf: url) + } + + /// Save the custom filter list rules + public func save(customRules: String) async throws { + let lines = customRules.components(separatedBy: .newlines) + if lines.count > Self.maxNumberOfCustomRulesLines { + throw CustomRulesError.tooManyLines(max: Self.maxNumberOfCustomRulesLines) + } + + if let failure = await ContentBlockerManager.shared.testRules(forFilterSet: customRules) { + throw CustomRulesError.failedRule( + rule: failure.rule, + line: failure.line, + error: failure.error + ) + } + + let fileURL = try customRulesFileURL() + if FileManager.default.fileExists(atPath: fileURL.path) { + try FileManager.default.removeItem(at: fileURL) + } + + try customRules.write(to: fileURL, atomically: true, encoding: .utf8) + Self.customRuleListVersion.value += 1 + guard let fileInfo = try savedCustomRulesFileInfo() else { return } + await AdBlockGroupsManager.shared.updateImmediately(fileInfo: fileInfo) + } + + /// Delete the saved custom filter list rules + func deleteCustomRules() async throws { + guard let fileURL = try savedCustomRulesFileURL() else { return } + try FileManager.default.removeItem(at: fileURL) + await AdBlockGroupsManager.shared.removeFileInfoImmediately(for: .filterListText) + } + + /// Get or create a cache folder for the given `Resource` + /// + /// - Note: This technically can't really return nil as the location and folder are hard coded + private func getOrCreateCustomRulesFolder() throws -> URL { + guard + let folderURL = FileManager.default.getOrCreateFolder( + name: "custom_rules", + location: .applicationDirectory + ) + else { + throw ResourceFileError.failedToCreateCacheFolder + } + + return folderURL + } + func update(filterListId id: ObjectIdentifier, with result: Result) { guard let index = filterListsURLs.firstIndex(where: { $0.id == id }) else { return diff --git a/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift b/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift index 59a3f38f01040..edf37fe2cab6c 100644 --- a/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift +++ b/ios/brave-ios/Sources/BraveShields/ShieldStrings.swift @@ -318,6 +318,210 @@ extension Strings.Shields { ) } +// MARK: - Filter lists + +extension Strings.Shields { + public static let contentFiltering = NSLocalizedString( + "ContentFiltering", + tableName: "BraveShared", + bundle: .module, + value: "Content Filtering", + comment: + "A title to the content filtering page under global shield settings and the title on the Content filtering page" + ) + public static let blockMobileAnnoyances = NSLocalizedString( + "blockMobileAnnoyances", + tableName: "BraveShared", + bundle: .module, + value: "Block 'Switch to App' Notices", + comment: "A title for setting which blocks 'switch to app' popups" + ) + public static let contentFilteringDescription = NSLocalizedString( + "ContentFilteringDescription", + tableName: "BraveShared", + bundle: .module, + value: + "Enable custom filters that block regional and language-specific trackers and Annoyances", + comment: "A description of the content filtering page." + ) + public static let defaultFilterLists = NSLocalizedString( + "DefaultFilterLists", + tableName: "BraveShared", + bundle: .module, + value: "Default Filter Lists", + comment: + "A section title that contains default (predefined) filter lists a user can enable/diable." + ) + public static let filterListsDescription = NSLocalizedString( + "FilterListsDescription", + tableName: "BraveShared", + bundle: .module, + value: + "Additional popular community lists. Note that enabling too many filters will degrade browsing speeds.", + comment: "A description on the content filtering screen for the filter lists section." + ) + public static let addCustomFilterList = NSLocalizedString( + "AddCustomFilterList", + tableName: "BraveShared", + bundle: .module, + value: "Add Custom Filter List", + comment: "A title within a cell where a user can navigate to an add screen." + ) + public static let customFilterList = NSLocalizedString( + "CustomFilterList", + tableName: "BraveShared", + bundle: .module, + value: "Custom Filter List", + comment: "Title for the custom filter list add screen found in the navigation bar." + ) + public static let customFilterLists = NSLocalizedString( + "CustomFilterLists", + tableName: "BraveShared", + bundle: .module, + value: "Custom Filter Lists", + comment: "A title for a section that contains all custom filter lists" + ) + public static let customFilterListURL = NSLocalizedString( + "CustomFilterListsURL", + tableName: "BraveShared", + bundle: .module, + value: "Custom Filter List URL", + comment: "A section heading above a cell that allows you to enter a filter list URL." + ) + public static let addCustomFilterListDescription = NSLocalizedString( + "AddCustomFilterListDescription", + tableName: "BraveShared", + bundle: .module, + value: "Add additional lists created and maintained by your trusted community.", + comment: + "A description of a section in a list that allows you to add custom filter lists found in the footer of the add custom url screen" + ) + public static let addCustomFilterListWarning = NSLocalizedString( + "AddCustomFilterListWarning", + tableName: "BraveShared", + bundle: .module, + value: + "**Only subscribe to lists from entities you trust**. Your browser will periodically check for list updates from the URL you enter.", + comment: "Warning text found in the footer of the add custom filter list url screen." + ) + public static let filterListsLastUpdated = NSLocalizedString( + "FilterListsLastUpdatedLabel", + tableName: "BraveShared", + bundle: .module, + value: "Last updated %@", + comment: + "A label that shows when the filter list was last updated. Do not translate the '%@' placeholder. The %@ will be replaced with a relative date. For example, '5 minutes ago' or '1 hour ago'. So the full string will read something like 'Last updated 5 minutes ago'." + ) + public static let filterListsDownloadPending = NSLocalizedString( + "FilterListsDownloadPending", + tableName: "BraveShared", + bundle: .module, + value: "Pending download", + comment: + "If a filter list is not yet downloaded this label shows up instead of a last download date, signifying that the download is still pending." + ) + public static let filterListsEnterFilterListURL = NSLocalizedString( + "FilterListsEnterFilterListURL", + tableName: "BraveShared", + bundle: .module, + value: "Enter filter list URL", + comment: "This is a placeholder for an input field that takes a custom filter list URL." + ) + public static let filterListsAdd = NSLocalizedString( + "FilterListsAdd", + tableName: "BraveShared", + bundle: .module, + value: "Add", + comment: + "This is a button on the top navigation that takes the user to an add custom filter list url to the list" + ) + public static let filterListsEdit = NSLocalizedString( + "FilterListsEdit", + tableName: "BraveShared", + bundle: .module, + value: "Edit", + comment: + "This is a button on the top navigation that takes the user to an add custom filter list url to the list" + ) + public static let filterListURLTextFieldPlaceholder = NSLocalizedString( + "FilterListURLTextFieldPlaceholder", + tableName: "BraveShared", + bundle: .module, + value: "Enter filter list URL here ", + comment: + "This is a placeholder for the custom filter list url text field where a user may enter a custom filter list URL" + ) + public static let filterListsDownloadFailed = NSLocalizedString( + "FilterListsDownloadFailed", + tableName: "BraveShared", + bundle: .module, + value: "Download failed", + comment: "This is a generic error message when downloading a filter list fails." + ) + public static let filterListAddInvalidURLError = NSLocalizedString( + "FilterListAddInvalidURLError", + tableName: "BraveShared", + bundle: .module, + value: "The URL entered is invalid", + comment: + "This is an error message when a user tries to enter an invalid URL into the custom filter list URL text field." + ) + public static let filterListAddOnlyHTTPSAllowedError = NSLocalizedString( + "FilterListAddOnlyHTTPSAllowedError", + tableName: "BraveShared", + bundle: .module, + value: "Only secure (https) URLs are allowed for custom filter lists", + comment: + "This is an error message when a user tries to enter a non-https scheme URL into the 'add custom filter list URL' input field" + ) +} + +// MARK: - Create custom filters + +extension Strings.Shields { + public static let customFilters = NSLocalizedString( + "CustomFilters", + tableName: "BraveShared", + bundle: .module, + value: "Custom Filters", + comment: "A title for a section that allows a user to insert custom filter list text" + ) + public static let customFiltersDescription = NSLocalizedString( + "CustomFiltersDescription", + tableName: "BraveShared", + bundle: .module, + value: + "Add custom filters you've created, one per line, in the field below. Be sure to use the Adblock filter syntax.", + comment: "A description of what to enter in the create custom filters text field" + ) + /// A placeholder when custom filter lists are empty + public static let customFiltersPlaceholder = NSLocalizedString( + "CustomFiltersPlaceholder", + tableName: "BraveShared", + bundle: .module, + value: "Add your custom filters here", + comment: "A placeholder when custom filter lists are empty" + ) + /// An error message telling the user that they crossed the line limit + public static let customFiltersTooManyLinesError = NSLocalizedString( + "CustomFiltersTooManyLinesError", + tableName: "BraveShared", + bundle: .module, + value: "Custom filters do not support more than %i lines", + comment: + "An error message telling the user that they crossed the line limit" + ) + /// An error message telling the user that they crossed the line limit + public static let customFiltersInvalidRuleError = NSLocalizedString( + "CustomFiltersInvalidRuleError", + tableName: "BraveShared", + bundle: .module, + value: "Invalid rule `%@` on line %i", + comment: + "An error message telling the user that a rule is invalid" + ) +} + // MARK: - HTTPS Upgrades extension Strings.Shields { diff --git a/ios/brave-ios/Sources/BraveStrings/BraveStrings.swift b/ios/brave-ios/Sources/BraveStrings/BraveStrings.swift index b92988e852181..58e1fbd0db190 100644 --- a/ios/brave-ios/Sources/BraveStrings/BraveStrings.swift +++ b/ios/brave-ios/Sources/BraveStrings/BraveStrings.swift @@ -16,6 +16,33 @@ extension Strings { value: "Cancel", comment: "" ) + /// A confirmation title that appears in an alert to dismiss changes in a form or text input + public static let dismissChangesConfirmationTitle = NSLocalizedString( + "DismissChangesConfirmationTitle", + tableName: "BraveShared", + bundle: .module, + value: "Dismiss Changes?", + comment: + "A confirmation title that appears in an alert to dismiss changes in a form or text input" + ) + /// A confirmation message that appears in an alert to dismiss changes in a form or text input + public static let dismissChangesConfirmationMessage = NSLocalizedString( + "DismissChangesConfirmationMessage", + tableName: "BraveShared", + bundle: .module, + value: "Are you sure you want to dismiss these changes? Any modifications will be lost.", + comment: + "A confirmation message that appears in an alert to dismiss changes in a form or text input" + ) + /// A confirmation button title for a alert option to dismiss changes in a form or text input + public static let dismissChangesButtonTitle = NSLocalizedString( + "DismissChangesButtonTitle", + tableName: "BraveShared", + bundle: .module, + value: "Dismiss Changes", + comment: + "A confirmation button title for a alert option to dismiss changes in a form or text input" + ) public static let unlockButtonTitle = NSLocalizedString( "UnlockButtonTitle", tableName: "BraveShared", @@ -72,6 +99,13 @@ extension Strings { value: "Save", comment: "Label for the button used to save data" ) + public static let saveChangesButtonTitle = NSLocalizedString( + "SaveChangesButtonTitle", + tableName: "BraveShared", + bundle: .module, + value: "Save changes", + comment: "Label for the button used to save changes in some text input." + ) public static let share = NSLocalizedString( "CommonShare", tableName: "BraveShared", @@ -11187,161 +11221,3 @@ extension Strings { ) } } - -// MARK: - Filter lists - -extension Strings { - public static let contentFiltering = NSLocalizedString( - "ContentFiltering", - tableName: "BraveShared", - bundle: .module, - value: "Content Filtering", - comment: - "A title to the content filtering page under global shield settings and the title on the Content filtering page" - ) - public static let blockMobileAnnoyances = NSLocalizedString( - "blockMobileAnnoyances", - tableName: "BraveShared", - bundle: .module, - value: "Block 'Switch to App' Notices", - comment: "A title for setting which blocks 'switch to app' popups" - ) - public static let contentFilteringDescription = NSLocalizedString( - "ContentFilteringDescription", - tableName: "BraveShared", - bundle: .module, - value: - "Enable custom filters that block regional and language-specific trackers and Annoyances", - comment: "A description of the content filtering page." - ) - public static let defaultFilterLists = NSLocalizedString( - "DefaultFilterLists", - tableName: "BraveShared", - bundle: .module, - value: "Default Filter Lists", - comment: - "A section title that contains default (predefined) filter lists a user can enable/diable." - ) - public static let filterListsDescription = NSLocalizedString( - "FilterListsDescription", - tableName: "BraveShared", - bundle: .module, - value: - "Additional popular community lists. Note that enabling too many filters will degrade browsing speeds.", - comment: "A description on the content filtering screen for the filter lists section." - ) - public static let addCustomFilterList = NSLocalizedString( - "AddCustomFilterList", - tableName: "BraveShared", - bundle: .module, - value: "Add Custom Filter List", - comment: "A title within a cell where a user can navigate to an add screen." - ) - public static let customFilterList = NSLocalizedString( - "CustomFilterList", - tableName: "BraveShared", - bundle: .module, - value: "Custom Filter List", - comment: "Title for the custom filter list add screen found in the navigation bar." - ) - public static let customFilterLists = NSLocalizedString( - "CustomFilterLists", - tableName: "BraveShared", - bundle: .module, - value: "Custom Filter Lists", - comment: "A title for a section that contains all custom filter lists" - ) - public static let customFilterListURL = NSLocalizedString( - "CustomFilterListsURL", - tableName: "BraveShared", - bundle: .module, - value: "Custom Filter List URL", - comment: "A section heading above a cell that allows you to enter a filter list URL." - ) - public static let addCustomFilterListDescription = NSLocalizedString( - "AddCustomFilterListDescription", - tableName: "BraveShared", - bundle: .module, - value: "Add additional lists created and maintained by your trusted community.", - comment: - "A description of a section in a list that allows you to add custom filter lists found in the footer of the add custom url screen" - ) - public static let addCustomFilterListWarning = NSLocalizedString( - "AddCustomFilterListWarning", - tableName: "BraveShared", - bundle: .module, - value: - "**Only subscribe to lists from entities you trust**. Your browser will periodically check for list updates from the URL you enter.", - comment: "Warning text found in the footer of the add custom filter list url screen." - ) - public static let filterListsLastUpdated = NSLocalizedString( - "FilterListsLastUpdatedLabel", - tableName: "BraveShared", - bundle: .module, - value: "Last updated %@", - comment: - "A label that shows when the filter list was last updated. Do not translate the '%@' placeholder. The %@ will be replaced with a relative date. For example, '5 minutes ago' or '1 hour ago'. So the full string will read something like 'Last updated 5 minutes ago'." - ) - public static let filterListsDownloadPending = NSLocalizedString( - "FilterListsDownloadPending", - tableName: "BraveShared", - bundle: .module, - value: "Pending download", - comment: - "If a filter list is not yet downloaded this label shows up instead of a last download date, signifying that the download is still pending." - ) - public static let filterListsEnterFilterListURL = NSLocalizedString( - "FilterListsEnterFilterListURL", - tableName: "BraveShared", - bundle: .module, - value: "Enter filter list URL", - comment: "This is a placeholder for an input field that takes a custom filter list URL." - ) - public static let filterListsAdd = NSLocalizedString( - "FilterListsAdd", - tableName: "BraveShared", - bundle: .module, - value: "Add", - comment: - "This is a button on the top navigation that takes the user to an add custom filter list url to the list" - ) - public static let filterListsEdit = NSLocalizedString( - "FilterListsEdit", - tableName: "BraveShared", - bundle: .module, - value: "Edit", - comment: - "This is a button on the top navigation that takes the user to an add custom filter list url to the list" - ) - public static let filterListURLTextFieldPlaceholder = NSLocalizedString( - "FilterListURLTextFieldPlaceholder", - tableName: "BraveShared", - bundle: .module, - value: "Enter filter list URL here ", - comment: - "This is a placeholder for the custom filter list url text field where a user may enter a custom filter list URL" - ) - public static let filterListsDownloadFailed = NSLocalizedString( - "FilterListsDownloadFailed", - tableName: "BraveShared", - bundle: .module, - value: "Download failed", - comment: "This is a generic error message when downloading a filter list fails." - ) - public static let filterListAddInvalidURLError = NSLocalizedString( - "FilterListAddInvalidURLError", - tableName: "BraveShared", - bundle: .module, - value: "The URL entered is invalid", - comment: - "This is an error message when a user tries to enter an invalid URL into the custom filter list URL text field." - ) - public static let filterListAddOnlyHTTPSAllowedError = NSLocalizedString( - "FilterListAddOnlyHTTPSAllowedError", - tableName: "BraveShared", - bundle: .module, - value: "Only secure (https) URLs are allowed for custom filter lists", - comment: - "This is an error message when a user tries to enter a non-https scheme URL into the 'add custom filter list URL' input field" - ) -} diff --git a/ios/brave-ios/Sources/BraveUI/Extensions/GeometryExtensions.swift b/ios/brave-ios/Sources/BraveUI/Extensions/GeometryExtensions.swift index 5c57ad5fc7964..9ab0b697101c4 100644 --- a/ios/brave-ios/Sources/BraveUI/Extensions/GeometryExtensions.swift +++ b/ios/brave-ios/Sources/BraveUI/Extensions/GeometryExtensions.swift @@ -23,4 +23,13 @@ extension UIEdgeInsets { right = inset bottom = inset } + + public init(vertical: CGFloat, horizontal: CGFloat) { + self.init( + top: vertical, + left: horizontal, + bottom: vertical, + right: horizontal + ) + } } diff --git a/ios/brave-ios/Tests/ClientTests/AdBlockGroupsManagerTests.swift b/ios/brave-ios/Tests/ClientTests/AdBlockGroupsManagerTests.swift index 5327c5de36182..2f0b49956ff04 100644 --- a/ios/brave-ios/Tests/ClientTests/AdBlockGroupsManagerTests.swift +++ b/ios/brave-ios/Tests/ClientTests/AdBlockGroupsManagerTests.swift @@ -299,13 +299,6 @@ class TestSourceProvider: AdBlockGroupsManager.SourceProvider { } } - /// Return all engabled sources for the given engine type - func enabledSources( - for engineType: GroupedAdBlockEngine.EngineType - ) -> [GroupedAdBlockEngine.Source] { - return enabledSources - } - func legacyCacheFiles( for engineType: Brave.GroupedAdBlockEngine.EngineType ) -> [Brave.AdBlockEngineManager.FileInfo] { diff --git a/ios/brave-ios/Tests/ClientTests/ContentBlockerManagerTests.swift b/ios/brave-ios/Tests/ClientTests/ContentBlockerManagerTests.swift index d33e5495503d8..7173d20d7f54c 100644 --- a/ios/brave-ios/Tests/ClientTests/ContentBlockerManagerTests.swift +++ b/ios/brave-ios/Tests/ClientTests/ContentBlockerManagerTests.swift @@ -43,7 +43,7 @@ class ContentBlockerManagerTests: XCTestCase { version: "0", modes: filterListType.allowedModes ) - let customListType = ContentBlockerManager.BlocklistType.customFilterList( + let customListType = ContentBlockerManager.BlocklistType.filterListURL( uuid: filterListCustomUUID ) try await manager.compile( @@ -88,7 +88,7 @@ class ContentBlockerManagerTests: XCTestCase { try await manager.removeRuleLists( for: .filterList(componentId: filterListUUID, isAlwaysAggressive: false) ) - try await manager.removeRuleLists(for: .customFilterList(uuid: filterListCustomUUID)) + try await manager.removeRuleLists(for: .filterListURL(uuid: filterListCustomUUID)) } catch { XCTFail(error.localizedDescription) } @@ -108,12 +108,52 @@ class ContentBlockerManagerTests: XCTestCase { await manager.compileRuleList( at: filterListURL, - for: .customFilterList(uuid: "iodkpdagapdfkphljnddpjlldadblomo"), + for: .filterListURL(uuid: "iodkpdagapdfkphljnddpjlldadblomo"), version: "0", modes: ContentBlockerManager.BlockingMode.allCases ) } + func testRulesTestingSuccess() async { + let filterSet = [ + "! This is a network rule", + "||example.com^", + "! This is an empty line", + "", + "! This is a cosmetic filter", + "example.com,example.net##h1", + ] + + let manager = await makeManager() + let result = await manager.testRules( + forFilterSet: filterSet.joined(separator: "\n") + ) + XCTAssertNil(result) + } + + func testRulesTestingError() async { + let filterSet = [ + "! This is a network rule", + "||example.com^", + "! This is an empty line", + "", + "! This is a cosmetic filter", + "example.com,example.net##h1", + "! This is an invalid rule", + "||video.twimg.com/ext_tw_video/*/*.m3u8$domain=/^i[a-z]*\\.strmrdr[a-z]+\\..*/", + ] + + let manager = await makeManager() + let result = await manager.testRules( + forFilterSet: filterSet.joined(separator: "\n") + ) + XCTAssertEqual(result?.line, 7) + XCTAssertEqual( + result?.rule, + "||video.twimg.com/ext_tw_video/*/*.m3u8$domain=/^i[a-z]*\\.strmrdr[a-z]+\\..*/" + ) + } + @MainActor private func makeManager() -> ContentBlockerManager { return ContentBlockerManager( ruleStore: ruleStore,