Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #6841: Add custom filter list urls
Browse files Browse the repository at this point in the history
  • Loading branch information
cuba committed Mar 22, 2023
1 parent 51e2e33 commit 278ac69
Show file tree
Hide file tree
Showing 16 changed files with 859 additions and 18 deletions.
8 changes: 7 additions & 1 deletion Sources/Brave/Frontend/Browser/Helpers/LaunchHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public actor LaunchHelper {
async let loadCaches: Void = ContentBlockerManager.shared.loadCaches()
async let filterListCache: Void = FilterListResourceDownloader.shared.loadCachedData()
async let adblockResourceCache: Void = AdblockResourceDownloader.shared.loadCachedData()
_ = await (loadCaches, filterListCache, adblockResourceCache)
async let filterListURLCache: Void = FilterListCustomURLDownloader.shared.loadCachedFilterLists()
_ = await (loadCaches, filterListCache, adblockResourceCache, filterListURLCache)

// Compile some engines
await AdBlockEngineManager.shared.compileResources()
Expand All @@ -45,6 +46,7 @@ public actor LaunchHelper {
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
ContentBlockerManager.log.debug("Adblock loaded: \(timeElapsed)s")
#endif

// This one is non-blocking
performPostLoadTasks(adBlockService: adBlockService)
areAdBlockServicesReady = true
Expand Down Expand Up @@ -81,5 +83,9 @@ public actor LaunchHelper {
.union(
ContentBlockerManager.GenericBlocklistType.allCases.map { .generic($0) }
)
// All custom filter list urls
.union(
CustomFilterListStorage.shared.filterListsURLs.map { .customFilterList(uuid: $0.setting.uuid) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ class AdblockDebugMenuTableViewController: TableViewController {
resources: FilterListResourceDownloader.shared.filterLists.map({ filterList in
return filterList.makeResource(componentId: filterList.entry.componentId)
})
),
self.downloadedResourcesSection(
header: "Filter list custom URLs",
footer: "Files downloaded using the FilterListURLResourceDownloader",
resources: CustomFilterListStorage.shared.filterListsURLs.map({ filterListURL in
return filterListURL.setting.resource
})
)
]
}
Expand Down
111 changes: 111 additions & 0 deletions Sources/Brave/Frontend/Settings/FilterListAddURLView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2023 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 http://mozilla.org/MPL/2.0/.

import SwiftUI
import Strings
import DesignSystem
import BraveUI

struct FilterListAddURLView: View {
@ObservedObject private var customFilterListStorage = CustomFilterListStorage.shared
@Environment(\.presentationMode) @Binding private var presentationMode
@State private var newURLInput: String = ""
@State private var errorMessage: String?

private var textField: some View {
TextField(Strings.filterListsEnterFilterListURL, text: $newURLInput)
.onChange(of: newURLInput) { newValue in
errorMessage = nil
}
.keyboardType(.URL)
.textContentType(.URL)
.autocapitalization(.none)
.autocorrectionDisabled()
}

var body: some View {
NavigationView {
List {
Section(content: {
VStack(alignment: .leading) {
textField
.submitLabel(SubmitLabel.done)

if let errorMessage = errorMessage {
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
}
}
}, header: {
Text(Strings.customFilterListURL)
}, footer: {
VStack(alignment: .leading, spacing: 8) {
Text(Strings.addCustomFilterListDescription)
Text(LocalizedStringKey(Strings.addCustomFilterListWarning))
}.padding(.top, 8)
})
}
.listBackgroundColor(Color(UIColor.braveGroupedBackground))
.navigationTitle(Strings.customFilterList)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .confirmationAction) {
Button(Strings.filterListsAdd) {
handleOnSubmit()
}.disabled(newURLInput.isEmpty)
}

ToolbarItemGroup(placement: .cancellationAction) {
Button(Strings.CancelString) {
presentationMode.dismiss()
}
}
}
}.frame(idealWidth: 400, idealHeight: 400)
}

private func handleOnSubmit() {
guard !newURLInput.isEmpty else { return }
guard let url = URL(string: newURLInput) else {
self.errorMessage = Strings.filterListAddInvalidURLError
return
}
guard url.scheme == "https" else {
self.errorMessage = Strings.filterListAddOnlyHTTPSAllowedError
return
}
guard !customFilterListStorage.filterListsURLs.contains(where: { filterListURL in
return filterListURL.setting.externalURL == url
}) else {
// Don't allow duplicates
self.presentationMode.dismiss()
return
}

Task {
let customURL = FilterListCustomURL(
externalURL: url, isEnabled: true,
inMemory: !customFilterListStorage.persistChanges
)

customFilterListStorage.filterListsURLs.append(customURL)

await FilterListCustomURLDownloader.shared.startFetching(
filterListCustomURL: customURL
)

self.presentationMode.dismiss()
}
}
}

#if DEBUG
struct FilterListAddURLView_Previews: PreviewProvider {
static var previews: some View {
FilterListAddURLView()
}
}
#endif
119 changes: 116 additions & 3 deletions Sources/Brave/Frontend/Settings/FilterListsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,79 @@ import Strings
import Data
import DesignSystem
import BraveUI
import BraveCore

/// A view showing enabled and disabled community filter lists
struct FilterListsView: View {
@ObservedObject private var filterListDownloader = FilterListResourceDownloader.shared
@ObservedObject private var customFilterListStorage = CustomFilterListStorage.shared
@Environment(\.editMode) private var editMode
@State private var showingAddSheet = false
private let dateFormatter = RelativeDateTimeFormatter()

var body: some View {
List {
Section {
ForEach($customFilterListStorage.filterListsURLs) { $filterListURL in
VStack(alignment: .leading, spacing: 4) {
Toggle(isOn: $filterListURL.setting.isEnabled) {
VStack(alignment: .leading, spacing: 4) {
Text(filterListURL.title ?? filterListURL.setting.externalURL.origin.url?.absoluteString ?? filterListURL.setting.externalURL.absoluteString)
.foregroundColor(Color(.bravePrimary))
.truncationMode(.middle)
.lineLimit(1)

switch filterListURL.downloadStatus {
case .downloaded(let downloadDate):
Text(String.localizedStringWithFormat(
Strings.filterListsLastUpdated,
dateFormatter.localizedString(for: downloadDate, relativeTo: Date())))
.font(.caption)
.foregroundColor(Color(.braveLabel))
case .failure:
Text(Strings.filterListsDownloadFailed)
.font(.caption)
.foregroundColor(.red)
case .pending:
Text(Strings.filterListsDownloadPending)
.font(.caption)
.foregroundColor(Color(.braveLabel))
}
}
}
.disabled(editMode?.wrappedValue.isEditing == true)
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onChange(of: filterListURL.setting.isEnabled) { value in
Task {
CustomFilterListSetting.save(inMemory: !customFilterListStorage.persistChanges)
await FilterListCustomURLDownloader.shared.handleUpdate(to: filterListURL, isEnabled: value)
}
}

Text(filterListURL.setting.externalURL.absoluteDisplayString)
.font(.caption)
.foregroundColor(Color(.secondaryBraveLabel))
.allowsTightening(true)
}.listRowBackground(Color(.secondaryBraveGroupedBackground))
}
.onDelete(perform: onDeleteHandling)

Button {
showingAddSheet = true
} label: {
Text(Strings.addCustomFilterList)
.foregroundColor(Color(.braveBlurple))
}
.buttonStyle(PlainButtonStyle())
.disabled(editMode?.wrappedValue.isEditing == true)
.listRowBackground(Color(.secondaryBraveGroupedBackground))
.popover(isPresented: $showingAddSheet, content: {
FilterListAddURLView()
})
} header: {
Text(Strings.customFilterLists)
}

Section {
ForEach($filterListDownloader.filterLists) { $filterList in
Toggle(isOn: $filterList.isEnabled) {
Expand All @@ -29,13 +95,60 @@ struct FilterListsView: View {
.listRowBackground(Color(.secondaryBraveGroupedBackground))
}
} header: {
Text(Strings.filterListsDescription)
.textCase(.none)
VStack(alignment: .leading, spacing: 4) {
Text(Strings.defaultFilterLists)
.textCase(.uppercase)
Text(Strings.filterListsDescription)
.textCase(.none)
}
}

}
.listBackgroundColor(Color(UIColor.braveGroupedBackground))
.listStyle(.insetGrouped)
.navigationTitle(Strings.filterLists)
.toolbar {
EditButton().disabled(
customFilterListStorage.filterListsURLs.isEmpty &&
editMode?.wrappedValue.isEditing == false
)
}
}

private func onDeleteHandling(offsets: IndexSet) {
let removedURLs = offsets.map { customFilterListStorage.filterListsURLs[$0] }
customFilterListStorage.filterListsURLs.remove(atOffsets: offsets)

Task {
await removedURLs.asyncConcurrentForEach { removedURL in
// 1. Disable the filter list.
// It would be better to delete it but for some reason if we remove the rule list,
// it will not allow us to remove it from the tab
// So we don't remove it, only flag it as disabled and it will be removed on the next launch
// during the `cleaupInvalidRuleLists` step on `LaunchHelper`
await FilterListCustomURLDownloader.shared.handleUpdate(
to: removedURL, isEnabled: false
)

// 2. Stop downloading the file
await FilterListCustomURLDownloader.shared.stopFetching(
filterListCustomURL: removedURL
)

// 3. Remove the files
do {
try removedURL.setting.resource.removeCacheFolder()
} catch {
ContentBlockerManager.log.error(
"Failed to remove file for resource \(removedURL.setting.uuid)"
)
}

// 4. Remove the setting.
// This should always happen in the end
// because we need to access properties on the setting until then
removedURL.setting.delete(inMemory: !customFilterListStorage.persistChanges)
}
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/Brave/WebFilters/AdBlockEngineManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public actor AdBlockEngineManager: Sendable {
case adBlock
case cosmeticFilters
case filterList(uuid: String)
case filterListURL(uuid: String)

/// The order of this source relative to other sources.
///
Expand All @@ -27,6 +28,7 @@ public actor AdBlockEngineManager: Sendable {
case .adBlock: return 0
case .cosmeticFilters: return 3
case .filterList: return 100
case .filterListURL: return 200
}
}
}
Expand Down Expand Up @@ -224,6 +226,7 @@ extension AdBlockEngineManager.Source: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .filterList(let uuid): return "filterList(\(uuid))"
case .filterListURL(let uuid): return "filterListURL(\(uuid))"
case .adBlock: return "adBlock"
case .cosmeticFilters: return "cosmeticFilters"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,14 @@ actor ContentBlockerManager {
return .filterList(uuid: filterList.uuid)
}

return Set(genericRuleLists).union(additionalRuleLists)
// Get rule lists for custom filter lists
let customFilterLists = CustomFilterListStorage.shared.filterListsURLs
let customRuleLists = customFilterLists.compactMap { customURL -> BlocklistType? in
guard customURL.setting.isEnabled else { return nil }
return .customFilterList(uuid: customURL.setting.uuid)
}

return Set(genericRuleLists).union(additionalRuleLists).union(customRuleLists)
}

/// Return the enabled rule types for this domain and the enabled settings.
Expand Down
55 changes: 55 additions & 0 deletions Sources/Brave/WebFilters/CustomFilterListStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2023 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 http://mozilla.org/MPL/2.0/.

import Foundation
import Data

@MainActor class CustomFilterListStorage: ObservableObject {
static let shared = CustomFilterListStorage(persistChanges: true)
/// 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
@Published var filterListsURLs: [FilterListCustomURL]

init(persistChanges: Bool) {
self.persistChanges = persistChanges
self.filterListsURLs = []
}

func loadCachedFilterLists() {
let settings = CustomFilterListSetting.loadAllSettings(fromMemory: !persistChanges)

self.filterListsURLs = settings.map { setting in
let resource = setting.resource
let date = try? resource.creationDate()

if let date = date {
return FilterListCustomURL(setting: setting, downloadStatus: .downloaded(date))
} else {
return FilterListCustomURL(setting: setting, downloadStatus: .pending)
}
}
}

func update(filterListId id: ObjectIdentifier, with result: Result<Date, Error>) {
guard let index = filterListsURLs.firstIndex(where: { $0.id == id }) else {
return
}

switch result {
case .failure(let error):
#if DEBUG
let externalURL = filterListsURLs[index].setting.externalURL.absoluteString
ContentBlockerManager.log.error(
"Failed to download resource \(externalURL): \(String(describing: error))"
)
#endif

filterListsURLs[index].downloadStatus = .failure
case .success(let date):
filterListsURLs[index].downloadStatus = .downloaded(date)
}
}
}
Loading

0 comments on commit 278ac69

Please sign in to comment.