diff --git a/Sources/Brave/WebFilters/FilterList.swift b/Sources/Brave/WebFilters/FilterList.swift index 9ec4a3038c1..3ed03dcfa6c 100644 --- a/Sources/Brave/WebFilters/FilterList.swift +++ b/Sources/Brave/WebFilters/FilterList.swift @@ -46,6 +46,11 @@ struct FilterList: Identifiable { let entry: AdblockFilterListCatalogEntry var isEnabled: Bool = false + /// Tells us if this filter list is regional (i.e. if it contains language restrictions) + var isRegional: Bool { + return !entry.languages.isEmpty + } + /// Lets us know if this filter list is always aggressive. /// Aggressive filter lists are those that are non regional. var isAlwaysAggressive: Bool { !isRegional } diff --git a/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift b/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift index b5014d14b7a..eb719f4cabf 100644 --- a/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift +++ b/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift @@ -53,7 +53,7 @@ actor FilterListCustomURLDownloader: ObservableObject { } /// Load any custom filter lists from cache so they are ready to use and start fetching updates. - func start() async { + func startIfNeeded() async { guard !startedService else { return } self.startedService = true await CustomFilterListStorage.shared.loadCachedFilterLists() diff --git a/Sources/Brave/WebFilters/FilterListInterface.swift b/Sources/Brave/WebFilters/FilterListInterface.swift deleted file mode 100644 index f3b4d26d897..00000000000 --- a/Sources/Brave/WebFilters/FilterListInterface.swift +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2022 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 - -protocol FilterListInterface { - @MainActor var uuid: String { get } - @MainActor var debugTitle: String { get } -} - -extension FilterListSetting: FilterListInterface { - var debugTitle: String { - return "\(uuid) \(componentId ?? "unknown")" - } -} - -extension FilterList: FilterListInterface { - var uuid: String { entry.uuid } - - var debugTitle: String { - return "\(entry.title) \(entry.componentId)" - } - - var isRegional: Bool { - return !entry.languages.isEmpty - } -} diff --git a/Sources/Brave/WebFilters/FilterListResourceDownloader.swift b/Sources/Brave/WebFilters/FilterListResourceDownloader.swift index a667ad5b1f2..c6a9e102979 100644 --- a/Sources/Brave/WebFilters/FilterListResourceDownloader.swift +++ b/Sources/Brave/WebFilters/FilterListResourceDownloader.swift @@ -25,7 +25,7 @@ public actor FilterListResourceDownloader { private var adBlockServiceTasks: [String: Task] /// A marker that says that we loaded shield components for the first time. /// This boolean is used to configure this downloader only once after `AdBlockService` generic shields have been loaded. - private var loadedShieldComponents = false + private var registeredFilterLists = false /// The path to the resources file private(set) var resourcesInfo: CachedAdBlockEngine.ResourcesInfo? @@ -40,23 +40,27 @@ public actor FilterListResourceDownloader { /// - Warning: This method loads filter list settings. /// You need to wait for `DataController.shared.initializeOnce()` to be called first before invoking this method public func loadFilterListSettingsAndCachedData() async { - guard let folderURL = await FilterListSetting.makeFolderURL( - forFilterListFolderPath: Preferences.AppState.lastDefaultFilterListFolderPath.value - ), FileManager.default.fileExists(atPath: folderURL.path) else { - return + if let defaultFilterListFolderURL = await FilterListSetting.makeFolderURL( + forFilterListFolderPath: Preferences.AppState.lastFilterListCatalogueComponentFolderPath.value + ), FileManager.default.fileExists(atPath: defaultFilterListFolderURL.path), + let resourcesFolderURL = await FilterListSetting.makeFolderURL( + forFilterListFolderPath: Preferences.AppState.lastAdBlockResourcesFolderPath.value + ), FileManager.default.fileExists(atPath: resourcesFolderURL.path) { + let resourcesInfo = await didUpdateResourcesComponent(folderURL: resourcesFolderURL) + async let startedCustomFilterListsDownloader: Void = FilterListCustomURLDownloader.shared.startIfNeeded() + async let cachedFilterLists: Void = compileCachedFilterLists(resourcesInfo: resourcesInfo) + async let compileDefaultEngine: Void = compileDefaultEngine(defaultFilterListFolderURL: defaultFilterListFolderURL, resourcesInfo: resourcesInfo) + _ = await (startedCustomFilterListsDownloader, cachedFilterLists, compileDefaultEngine) + } else if let legacyComponentFolderURL = await FilterListSetting.makeFolderURL( + forFilterListFolderPath: Preferences.AppState.lastLegacyDefaultFilterListFolderPath.value + ), FileManager.default.fileExists(atPath: legacyComponentFolderURL.path) { + // TODO: @JS Remove this after this release. Its here just so users can upgrade without a pause to their adblocking + let resourcesInfo = await didUpdateResourcesComponent(folderURL: legacyComponentFolderURL) + async let startedCustomFilterListsDownloader: Void = FilterListCustomURLDownloader.shared.startIfNeeded() + async let cachedFilterLists: Void = compileCachedFilterLists(resourcesInfo: resourcesInfo) + async let compileDefaultEngine: Void = compileDefaultEngine(defaultFilterListFolderURL: legacyComponentFolderURL, resourcesInfo: resourcesInfo) + _ = await (startedCustomFilterListsDownloader, cachedFilterLists, compileDefaultEngine) } - - let version = folderURL.lastPathComponent - let resourcesInfo = CachedAdBlockEngine.ResourcesInfo( - localFileURL: folderURL.appendingPathComponent("resources.json", conformingTo: .json), - version: version - ) - self.resourcesInfo = resourcesInfo - - async let startedCustomFilterListsDownloader: Void = FilterListCustomURLDownloader.shared.start() - async let cachedFilterLists: Void = compileCachedFilterLists(resourcesInfo: resourcesInfo) - async let compileDefaultEngine: Void = compileDefaultEngine(shieldsInstallFolder: folderURL, resourcesInfo: resourcesInfo) - _ = await (startedCustomFilterListsDownloader, cachedFilterLists, compileDefaultEngine) } /// This function adds engine resources to `AdBlockManager` from cached data representing the enabled filter lists. @@ -103,59 +107,94 @@ public actor FilterListResourceDownloader { // Start listening to changes to the install url Task { @MainActor in - for await folderURL in adBlockService.shieldsInstallURL { - await self.didUpdateShieldComponent( - folderURL: folderURL, - adBlockFilterLists: adBlockService.regionalFilterLists ?? [] - ) + for await folderURL in adBlockService.resourcesComponentStream() { + guard let folderURL = folderURL else { + ContentBlockerManager.log.error("Missing folder for filter lists") + return + } + + await didUpdateResourcesComponent(folderURL: folderURL) + await FilterListCustomURLDownloader.shared.startIfNeeded() + + if !FilterListStorage.shared.filterLists.isEmpty { + await registerAllFilterListsIfNeeded(with: adBlockService) + } + } + } + + Task { @MainActor in + for await filterListEntries in adBlockService.filterListCatalogComponentStream() { + FilterListStorage.shared.loadFilterLists(from: filterListEntries) + + if await self.resourcesInfo != nil { + await registerAllFilterListsIfNeeded(with: adBlockService) + } } } } - /// Invoked when shield components are loaded - /// - /// This function will start fetching data and subscribe publishers once if it hasn't already done so. - private func didUpdateShieldComponent(folderURL: URL, adBlockFilterLists: [AdblockFilterListCatalogEntry]) async { - // Store the folder path so we can load it from cache next time we launch quicker - // than waiting for the component updater to respond, which may take a few seconds + /// Register all enabled filter lists and to the default filter list with the `AdBlockService` + private func registerAllFilterListsIfNeeded(with adBlockService: AdblockService) async { + guard !registeredFilterLists else { return } + self.registeredFilterLists = true + registerToDefaultFilterList(with: adBlockService) + + for filterList in await FilterListStorage.shared.filterLists { + register(filterList: filterList) + } + } + + /// Register to changes to the default filter list with the given ad-block service + private func registerToDefaultFilterList(with adBlockService: AdblockService) { + // Register the default filter list + Task { @MainActor in + for await folderURL in adBlockService.defaultComponentStream() { + guard let folderURL = folderURL else { + ContentBlockerManager.log.error("Missing folder for filter lists") + return + } + + await Task { @MainActor in + let folderSubPath = FilterListSetting.extractFolderPath(fromFilterListFolderURL: folderURL) + Preferences.AppState.lastFilterListCatalogueComponentFolderPath.value = folderSubPath + }.value + + if let resourcesInfo = await self.resourcesInfo { + await compileDefaultEngine(defaultFilterListFolderURL: folderURL, resourcesInfo: resourcesInfo) + } + } + } + } + + @discardableResult + /// When the + private func didUpdateResourcesComponent(folderURL: URL) async -> CachedAdBlockEngine.ResourcesInfo { await Task { @MainActor in let folderSubPath = FilterListSetting.extractFolderPath(fromFilterListFolderURL: folderURL) - Preferences.AppState.lastDefaultFilterListFolderPath.value = folderSubPath + Preferences.AppState.lastAdBlockResourcesFolderPath.value = folderSubPath }.value - // Set the resources info so other filter lists can use them let version = folderURL.lastPathComponent let resourcesInfo = CachedAdBlockEngine.ResourcesInfo( localFileURL: folderURL.appendingPathComponent("resources.json", conformingTo: .json), version: version ) - self.resourcesInfo = resourcesInfo - // Perform one time setup - if !loadedShieldComponents && !adBlockFilterLists.isEmpty { - // This is the first time we load ad-block filters. - // We need to perform some initial setup (but only do this once) - loadedShieldComponents = true - await FilterListStorage.shared.loadFilterLists(from: adBlockFilterLists) - - Task { - // Start the custom filter list downloader - await FilterListCustomURLDownloader.shared.start() - } - } - - // Compile the engine - await compileDefaultEngine(shieldsInstallFolder: folderURL, resourcesInfo: resourcesInfo) - await registerAllFilterLists() + self.resourcesInfo = resourcesInfo + return resourcesInfo } /// Compile the general engine from the given `AdblockService` `shieldsInstallPath` `URL`. - private func compileDefaultEngine(shieldsInstallFolder folderURL: URL, resourcesInfo: CachedAdBlockEngine.ResourcesInfo) async { + private func compileDefaultEngine(defaultFilterListFolderURL folderURL: URL, resourcesInfo: CachedAdBlockEngine.ResourcesInfo) async { + // TODO: @JS Remove this on the next update. This is here so users don't have a pause to their ad-blocking + let isLegacy = folderURL.pathExtension == "dat" + let localFileURL = isLegacy ? folderURL.appendingPathComponent("rs-ABPFilterParserData.dat", conformingTo: .data) : folderURL.appendingPathComponent("list.txt", conformingTo: .text) + let version = folderURL.lastPathComponent let filterListInfo = CachedAdBlockEngine.FilterListInfo( source: .adBlock, - localFileURL: folderURL.appendingPathComponent("rs-ABPFilterParserData.dat", conformingTo: .data), - version: version, fileType: .dat + localFileURL: localFileURL, + version: version, fileType: isLegacy ? .dat : .text ) guard await AdBlockStats.shared.needsCompilation(for: filterListInfo, resourcesInfo: resourcesInfo) else { @@ -206,13 +245,6 @@ public actor FilterListResourceDownloader { } } - /// Register all enabled filter lists with the `AdBlockService` - @MainActor private func registerAllFilterLists() async { - for filterList in FilterListStorage.shared.filterLists { - await register(filterList: filterList) - } - } - /// Register this filter list with the `AdBlockService` private func register(filterList: FilterList) { guard adBlockServiceTasks[filterList.entry.componentId] == nil else { return } @@ -235,7 +267,7 @@ public actor FilterListResourceDownloader { ) // Save the downloaded folder for later (caching) purposes - FilterListStorage.shared.set(folderURL: folderURL, forUUID: filterList.uuid) + FilterListStorage.shared.set(folderURL: folderURL, forUUID: filterList.entry.uuid) } } } @@ -307,32 +339,38 @@ public actor FilterListResourceDownloader { /// Helpful extension to the AdblockService private extension AdblockService { - /// Stream the URL updates to the `shieldsInstallPath` - /// - /// - Warning: You should never do this more than once. Only one callback can be registered to the `shieldsComponentReady` callback. - @MainActor var shieldsInstallURL: AsyncStream { + @MainActor func defaultComponentStream() -> AsyncStream { return AsyncStream { continuation in - if let folderPath = shieldsInstallPath { + registerDefaultComponent { folderPath in + guard let folderPath = folderPath else { + continuation.yield(nil) + return + } + let folderURL = URL(fileURLWithPath: folderPath) continuation.yield(folderURL) } - - guard shieldsComponentReady == nil else { - assertionFailure("You have already set the `shieldsComponentReady` callback. Setting this more than once replaces the previous callback.") - return - } - - shieldsComponentReady = { folderPath in + } + } + + @MainActor func resourcesComponentStream() -> AsyncStream { + return AsyncStream { continuation in + registerResourceComponent { folderPath in guard let folderPath = folderPath else { + continuation.yield(nil) return } let folderURL = URL(fileURLWithPath: folderPath) continuation.yield(folderURL) } - - continuation.onTermination = { @Sendable _ in - self.shieldsComponentReady = nil + } + } + + @MainActor func filterListCatalogComponentStream() -> AsyncStream<[AdblockFilterListCatalogEntry]> { + return AsyncStream { continuation in + registerFilterListCatalogComponent { filterListEntries in + continuation.yield(filterListEntries) } } } @@ -342,7 +380,7 @@ private extension AdblockService { /// - Note: Cancelling this task will unregister this filter list from recieving any further updates @MainActor func register(filterList: FilterList) -> AsyncStream { return AsyncStream { continuation in - registerFilterListComponent(filterList.entry, useLegacyComponent: false) { folderPath in + registerFilterListComponent(filterList.entry) { folderPath in guard let folderPath = folderPath else { continuation.yield(nil) return @@ -353,7 +391,7 @@ private extension AdblockService { } continuation.onTermination = { @Sendable _ in - self.unregisterFilterListComponent(filterList.entry, useLegacyComponent: true) + self.unregisterFilterListComponent(filterList.entry) } } } diff --git a/Sources/Brave/WebFilters/FilterListStorage.swift b/Sources/Brave/WebFilters/FilterListStorage.swift index eb6ae068e8d..b10e274b754 100644 --- a/Sources/Brave/WebFilters/FilterListStorage.swift +++ b/Sources/Brave/WebFilters/FilterListStorage.swift @@ -134,6 +134,7 @@ import Combine upsertSetting( uuid: filterList.entry.uuid, isEnabled: filterList.isEnabled, + isHidden: false, componentId: filterList.entry.componentId, allowCreation: true, order: filterList.order, @@ -146,7 +147,7 @@ import Combine /// /// - Warning: Do not call this before we load core data private func upsertSetting( - uuid: String, isEnabled: Bool, componentId: String, + uuid: String, isEnabled: Bool, isHidden: Bool, componentId: String, allowCreation: Bool, order: Int, isAlwaysAggressive: Bool ) { if allFilterListSettings.contains(where: { $0.uuid == uuid }) { @@ -154,6 +155,7 @@ import Combine uuid: uuid, componentId: componentId, isEnabled: isEnabled, + isHidden: isHidden, order: order, isAlwaysAggressive: isAlwaysAggressive ) @@ -162,6 +164,7 @@ import Combine uuid: uuid, componentId: componentId, isEnabled: isEnabled, + isHidden: isHidden, order: order, isAlwaysAggressive: isAlwaysAggressive ) @@ -183,29 +186,34 @@ import Combine /// Update the filter list settings with the given `componentId` and `isEnabled` status /// Will not write unless one of these two values have changed - private func updateSetting(uuid: String, componentId: String, isEnabled: Bool, order: Int, isAlwaysAggressive: Bool) { + private func updateSetting(uuid: String, componentId: String, isEnabled: Bool, isHidden: Bool, order: Int, isAlwaysAggressive: Bool) { guard let index = allFilterListSettings.firstIndex(where: { $0.uuid == uuid }) else { return } - guard allFilterListSettings[index].isEnabled != isEnabled || allFilterListSettings[index].componentId != componentId || allFilterListSettings[index].order?.intValue != order || allFilterListSettings[index].isAlwaysAggressive != isAlwaysAggressive else { - // Ensure we stop if this is already in sync in order to avoid an event loop - // And things hanging for too long. - // This happens because we care about UI changes but not when our downloads finish + // Ensure we stop if this is already in sync in order to avoid an event loop + // And things hanging for too long. + guard allFilterListSettings[index].isEnabled != isEnabled + || allFilterListSettings[index].componentId != componentId + || allFilterListSettings[index].order?.intValue != order + || allFilterListSettings[index].isAlwaysAggressive != isAlwaysAggressive + || allFilterListSettings[index].isHidden != isHidden + else { return } allFilterListSettings[index].isEnabled = isEnabled allFilterListSettings[index].isAlwaysAggressive = isAlwaysAggressive + allFilterListSettings[index].isHidden = isHidden allFilterListSettings[index].componentId = componentId allFilterListSettings[index].order = NSNumber(value: order) FilterListSetting.save(inMemory: !persistChanges) } /// Create a filter list setting for the given UUID and enabled status - private func create(uuid: String, componentId: String, isEnabled: Bool, order: Int, isAlwaysAggressive: Bool) { + private func create(uuid: String, componentId: String, isEnabled: Bool, isHidden: Bool, order: Int, isAlwaysAggressive: Bool) { let setting = FilterListSetting.create( - uuid: uuid, componentId: componentId, isEnabled: isEnabled, order: order, inMemory: !persistChanges, + uuid: uuid, componentId: componentId, isEnabled: isEnabled, isHidden: isHidden, order: order, inMemory: !persistChanges, isAlwaysAggressive: isAlwaysAggressive ) allFilterListSettings.append(setting) diff --git a/Sources/Data/models/FilterListSetting.swift b/Sources/Data/models/FilterListSetting.swift index a4b039fbf4b..c6de8459a2d 100644 --- a/Sources/Data/models/FilterListSetting.swift +++ b/Sources/Data/models/FilterListSetting.swift @@ -18,6 +18,7 @@ public final class FilterListSetting: NSManagedObject, CRUD { @MainActor @NSManaged public var uuid: String @MainActor @NSManaged public var componentId: String? @MainActor @NSManaged public var isEnabled: Bool + @MainActor @NSManaged public var isHidden: Bool @MainActor @NSManaged public var isAlwaysAggressive: Bool @MainActor @NSManaged public var order: NSNumber? @MainActor @NSManaged private var folderPath: String? @@ -39,7 +40,7 @@ public final class FilterListSetting: NSManagedObject, CRUD { /// Create a filter list setting for the given UUID and enabled status @MainActor public class func create( - uuid: String, componentId: String?, isEnabled: Bool, order: Int, inMemory: Bool, isAlwaysAggressive: Bool + uuid: String, componentId: String?, isEnabled: Bool, isHidden: Bool, order: Int, inMemory: Bool, isAlwaysAggressive: Bool ) -> FilterListSetting { var newSetting: FilterListSetting! @@ -52,6 +53,7 @@ public final class FilterListSetting: NSManagedObject, CRUD { newSetting.uuid = uuid newSetting.componentId = componentId newSetting.isEnabled = isEnabled + newSetting.isHidden = isHidden newSetting.isAlwaysAggressive = isAlwaysAggressive newSetting.order = NSNumber(value: order) } diff --git a/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion b/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion index 483172cbd80..bcbcdab8fd3 100644 --- a/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion +++ b/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model24.xcdatamodel + Model25.xcdatamodel diff --git a/Sources/Data/models/Model.xcdatamodeld/Model25.xcdatamodel/contents b/Sources/Data/models/Model.xcdatamodeld/Model25.xcdatamodel/contents new file mode 100644 index 00000000000..f375c275042 --- /dev/null +++ b/Sources/Data/models/Model.xcdatamodeld/Model25.xcdatamodel/contents @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/Preferences/GlobalPreferences.swift b/Sources/Preferences/GlobalPreferences.swift index 5c021541bd7..b0b014ca20d 100644 --- a/Sources/Preferences/GlobalPreferences.swift +++ b/Sources/Preferences/GlobalPreferences.swift @@ -121,8 +121,22 @@ extension Preferences { /// /// This is a useful setting because it take too long for filter lists to load during launch /// and therefore we can try to load them right away and have them ready on the first tab load - @MainActor public static let lastDefaultFilterListFolderPath = + @MainActor public static let lastLegacyDefaultFilterListFolderPath = Option(key: "caching.last-default-filter-list-folder-path", default: nil) + + /// A cached value for the last folder path we got for our ad-block resources + /// + /// This is a useful setting because it take too long for filter lists to load during launch + /// and therefore we can try to load them right away and have them ready on the first tab load + @MainActor public static let lastAdBlockResourcesFolderPath = + Option(key: "caching.last-ad-block-resources-folder-path", default: nil) + + /// A cached value for the last folder path we got our filter lists components + /// + /// This is a useful setting because it take too long for filter lists to load during launch + /// and therefore we can try to load them right away and have them ready on the first tab load + @MainActor public static let lastFilterListCatalogueComponentFolderPath = + Option(key: "caching.last-filter-list-catalogue-component-folder-path", default: nil) } public final class Chromium {