diff --git a/App/iOS/Delegates/AppDelegate.swift b/App/iOS/Delegates/AppDelegate.swift index 0f1c33fa096..90a05082f62 100644 --- a/App/iOS/Delegates/AppDelegate.swift +++ b/App/iOS/Delegates/AppDelegate.swift @@ -338,7 +338,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UrpLog.log("Failed to initialize user referral program") } - DebouncingResourceDownloader.shared.startLoading() #if canImport(BraveTalk) BraveTalkJitsiCoordinator.sendAppLifetimeEvent( .didFinishLaunching(options: launchOptions ?? [:]) diff --git a/App/iOS/Delegates/SceneDelegate.swift b/App/iOS/Delegates/SceneDelegate.swift index 1831e2e1719..7fba9a2aa15 100644 --- a/App/iOS/Delegates/SceneDelegate.swift +++ b/App/iOS/Delegates/SceneDelegate.swift @@ -217,11 +217,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneWillEnterForeground(_ scene: UIScene) { - // The reason we need to call this method here instead of `applicationDidBecomeActive` - // is that this method is only invoked whenever the application is entering the foreground where as - // `applicationDidBecomeActive` will get called whenever the Touch ID authentication overlay disappears. - DebouncingResourceDownloader.shared.startLoading() - if let scene = scene as? UIWindowScene { scene.browserViewController?.windowProtection = windowProtection } diff --git a/Sources/Brave/Frontend/Settings/AdblockDebugMenuTableViewController.swift b/Sources/Brave/Frontend/Settings/AdblockDebugMenuTableViewController.swift index 820ea88b5b9..ab214a8659d 100644 --- a/Sources/Brave/Frontend/Settings/AdblockDebugMenuTableViewController.swift +++ b/Sources/Brave/Frontend/Settings/AdblockDebugMenuTableViewController.swift @@ -32,7 +32,25 @@ class AdblockDebugMenuTableViewController: TableViewController { self.actionsSection, self.datesSection, self.bundledListsSection(names: listNames), - self.downloadedResourcesSection() + self.downloadedResourcesSection( + header: "Ad-Block Resources", + footer: "Files downloaded using the AdBlockResourceDownloader", + resources: AdblockResourceDownloader.handledResources + ), + self.downloadedResourcesSection( + header: "Filter lists", + footer: "Files downloaded using the FilterListResourceDownloader", + 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 + }) + ) ] } } @@ -137,12 +155,14 @@ class AdblockDebugMenuTableViewController: TableViewController { return section } - private func downloadedResourcesSection() -> Section { - func createRows(from resources: [ResourceDownloader.Resource]) -> [Row] { + private func downloadedResourcesSection( + header: Section.Extremity?, footer: Section.Extremity?, resources: [Resource] + ) -> Section { + func createRows(from resources: [Resource]) -> [Row] { resources.compactMap { createRow(from: $0) } } - func getEtag(from resource: ResourceDownloader.Resource) -> String? { + func getEtag(from resource: Resource) -> String? { do { return try ResourceDownloader.etag(for: resource) } catch { @@ -150,7 +170,7 @@ class AdblockDebugMenuTableViewController: TableViewController { } } - func getFileCreation(for resource: ResourceDownloader.Resource) -> String? { + func getFileCreation(for resource: Resource) -> String? { do { guard let date = try ResourceDownloader.creationDate(for: resource) else { return nil } return Self.fileDateFormatter.string(from: date) @@ -159,7 +179,7 @@ class AdblockDebugMenuTableViewController: TableViewController { } } - func createRow(from resource: ResourceDownloader.Resource) -> Row? { + func createRow(from resource: Resource) -> Row? { guard let fileURL = ResourceDownloader.downloadedFileURL(for: resource) else { return nil } @@ -173,22 +193,9 @@ class AdblockDebugMenuTableViewController: TableViewController { return Row(text: fileURL.lastPathComponent, detailText: detailText, cellClass: MultilineSubtitleCell.self) } - var resources = FilterListResourceDownloader.shared.filterLists.map { filterList -> ResourceDownloader.Resource in - return filterList.makeResource(componentId: filterList.entry.componentId) - } - - resources.append(contentsOf: CustomFilterListStorage.shared.filterListsURLs.map({ customURL in - return .customFilterListURL(uuid: customURL.setting.uuid, externalURL: customURL.setting.externalURL) - })) - - resources.append(contentsOf: [ - .debounceRules, .genericContentBlockingBehaviors, .genericFilterRules, .generalCosmeticFilters, .generalScriptletResources - ]) - return Section( - header: "Downloaded resources", - rows: createRows(from: resources), - footer: "Lists downloaded from the internet at app launch using the ResourceDownloader." + header: header, rows: createRows(from: resources), + footer: footer ) } } diff --git a/Sources/Brave/WebFilters/AdblockResourceDownloader.swift b/Sources/Brave/WebFilters/AdblockResourceDownloader.swift index 2d37b844ee5..deb86f4a90f 100644 --- a/Sources/Brave/WebFilters/AdblockResourceDownloader.swift +++ b/Sources/Brave/WebFilters/AdblockResourceDownloader.swift @@ -18,6 +18,11 @@ public actor AdblockResourceDownloader: Sendable { .blockTrackers, .blockCookies ] + /// All the different resources this downloader handles + static let handledResources: [BraveS3Resource] = [ + .genericContentBlockingBehaviors, .generalCosmeticFilters, .debounceRules + ] + /// A formatter that is used to format a version number private let fileVersionDateFormatter: DateFormatter = { let dateFormatter = DateFormatter() @@ -28,9 +33,9 @@ public actor AdblockResourceDownloader: Sendable { }() /// The resource downloader that will be used to download all our resoruces - private let resourceDownloader: ResourceDownloader + private let resourceDownloader: ResourceDownloader /// All the resources that this downloader handles - private let handledResources: [ResourceDownloader.Resource] = [.genericContentBlockingBehaviors, .generalCosmeticFilters] + init(networkManager: NetworkManager = NetworkManager()) { self.resourceDownloader = ResourceDownloader(networkManager: networkManager) @@ -50,7 +55,7 @@ public actor AdblockResourceDownloader: Sendable { } // Here we load downloaded resources if we need to - await handledResources.asyncConcurrentForEach { resource in + await Self.handledResources.asyncConcurrentForEach { resource in await self.loadCachedData(for: resource) } } @@ -59,13 +64,13 @@ public actor AdblockResourceDownloader: Sendable { public func startFetching() { let fetchInterval = AppConstants.buildChannel.isPublic ? 6.hours : 10.minutes - for resource in handledResources { + for resource in Self.handledResources { startFetching(resource: resource, every: fetchInterval) } } /// Start fetching the given resource at regular intervals - private func startFetching(resource: ResourceDownloader.Resource, every fetchInterval: TimeInterval) { + private func startFetching(resource: BraveS3Resource, every fetchInterval: TimeInterval) { Task { @MainActor in for try await result in await self.resourceDownloader.downloadStream(for: resource, every: fetchInterval) { switch result { @@ -79,7 +84,7 @@ public actor AdblockResourceDownloader: Sendable { } /// Load cached data for the given resource. Ensures this is done on the MainActor - private func loadCachedData(for resource: ResourceDownloader.Resource) async { + private func loadCachedData(for resource: BraveS3Resource) async { do { if let downloadResult = try ResourceDownloaderStream.downloadResult(for: resource) { await handle(downloadResult: downloadResult, for: resource) @@ -99,7 +104,7 @@ public actor AdblockResourceDownloader: Sendable { // But ensure that this is only triggered for handled resource // We should have never triggered this method for resources that are outside // of the handled resources list - assert(handledResources.contains(resource)) + assert(Self.handledResources.contains(resource)) } } } catch { @@ -108,17 +113,10 @@ public actor AdblockResourceDownloader: Sendable { } /// Handle the downloaded file url for the given resource - private func handle(downloadResult: ResourceDownloaderStream.DownloadResult, for resource: ResourceDownloader.Resource) async { + private func handle(downloadResult: ResourceDownloaderStream.DownloadResult, for resource: BraveS3Resource) async { let version = fileVersionDateFormatter.string(from: downloadResult.date) switch resource { - case .genericFilterRules: - await AdBlockEngineManager.shared.add( - resource: AdBlockEngineManager.Resource(type: .ruleList, source: .adBlock), - fileURL: downloadResult.fileURL, - version: version - ) - case .generalCosmeticFilters: await AdBlockEngineManager.shared.add( resource: AdBlockEngineManager.Resource(type: .dat, source: .cosmeticFilters), @@ -151,6 +149,23 @@ public actor AdblockResourceDownloader: Sendable { ContentBlockerManager.log.error("Failed to compile downloaded content blocker resource: \(error.localizedDescription)") } + case .debounceRules: + // We don't want to setup the debounce rules more than once for the same cached file + guard downloadResult.isModified || DebouncingResourceDownloader.shared.matcher == nil else { + return + } + + do { + guard let data = try ResourceDownloader.data(for: resource) else { + assertionFailure("We just downloaded this file, how can it not be there?") + return + } + + try DebouncingResourceDownloader.shared.setup(withRulesJSON: data) + } catch { + ContentBlockerManager.log.error("Failed to setup debounce rules: \(error.localizedDescription)") + } + default: assertionFailure("Should not be handling this resource type") } diff --git a/Sources/Brave/WebFilters/BraveS3Resource.swift b/Sources/Brave/WebFilters/BraveS3Resource.swift new file mode 100644 index 00000000000..a0a4ebd879b --- /dev/null +++ b/Sources/Brave/WebFilters/BraveS3Resource.swift @@ -0,0 +1,85 @@ +// 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 Shared + +enum BraveS3Resource: Hashable, DownloadResourceInterface { + /// Rules for debouncing links + case debounceRules + /// Generic iOS only content blocking behaviours used for the iOS content blocker + case genericContentBlockingBehaviors + /// Cosmetic filter rules + case generalCosmeticFilters + /// Adblock rules for a filter list + /// iOS only content blocking behaviours used for the iOS content blocker for a given filter list + case filterListContentBlockingBehaviors(uuid: String, componentId: String) + + /// The name of the info plist key that contains the service key + private static let servicesKeyName = "SERVICES_KEY" + /// The name of the header value that contains the service key + private static let servicesKeyHeaderValue = "BraveServiceKey" + /// The base s3 environment url that hosts the debouncing (and other) files. + /// Cannot be used as-is and must be combined with a path + private static var baseResourceURL: URL = { + if AppConstants.buildChannel.isPublic { + return URL(string: "https://adblock-data.s3.brave.com")! + } else { + return URL(string: "https://adblock-data-staging.s3.bravesoftware.com")! + } + }() + + /// The folder name under which this data should be saved under + var cacheFolderName: String { + switch self { + case .debounceRules: + return "debounce-data" + case .filterListContentBlockingBehaviors(_, let componentId): + return ["filter-lists", componentId].joined(separator: "/") + case .genericContentBlockingBehaviors: + return "abp-data" + case .generalCosmeticFilters: + return "cmf-data" + } + } + + /// Get the file name that is stored on the device + var cacheFileName: String { + switch self { + case .debounceRules: + return "ios-debouce.json" + case .filterListContentBlockingBehaviors(let uuid, _): + return "\(uuid)-latest.json" + case .genericContentBlockingBehaviors: + return "latest.json" + case .generalCosmeticFilters: + return "ios-cosmetic-filters.dat" + } + } + + /// Get the external path for the given filter list and this resource type + var externalURL: URL { + switch self { + case .debounceRules: + return Self.baseResourceURL.appendingPathComponent("/ios/debounce.json") + case .filterListContentBlockingBehaviors(let uuid, _): + return Self.baseResourceURL.appendingPathComponent("/ios/\(uuid)-latest.json") + case .genericContentBlockingBehaviors: + return Self.baseResourceURL.appendingPathComponent("/ios/latest.json") + case .generalCosmeticFilters: + return Self.baseResourceURL.appendingPathComponent("/ios/ios-cosmetic-filters.dat") + } + } + + var headers: [String: String] { + var headers = [String: String]() + + if let servicesKeyValue = Bundle.main.getPlistString(for: Self.servicesKeyName) { + headers[Self.servicesKeyHeaderValue] = servicesKeyValue + } + + return headers + } +} diff --git a/Sources/Brave/WebFilters/DebouncingResourceDownloader.swift b/Sources/Brave/WebFilters/DebouncingResourceDownloader.swift index a91be517a7d..fc1c94af478 100644 --- a/Sources/Brave/WebFilters/DebouncingResourceDownloader.swift +++ b/Sources/Brave/WebFilters/DebouncingResourceDownloader.swift @@ -465,24 +465,10 @@ public class DebouncingResourceDownloader { }() public static let shared = DebouncingResourceDownloader() - private let networkManager: NetworkManager - private let servicesKeyName = "SERVICES_KEY" - private let servicesKeyHeaderValue = "BraveServiceKey" - private let cacheFileName = "ios-debouce" - private let cacheFolderName = "debounce-data" - private var matcher: Matcher? - - /// A boolean indicating if this is a first time load of this downloader so we only load cached data once - private var initialLoad = true - /// Initialized with year 1970 to force adblock fetch at first launch. - private var lastFetchDate = Date(timeIntervalSince1970: 0) - /// How frequently to fetch the data - private lazy var fetchInterval = AppConstants.buildChannel.isPublic ? 6.hours : 10.minutes + private(set) var matcher: Matcher? /// Initialize this instance with a network manager - init(networkManager: NetworkManager = NetworkManager()) { - self.networkManager = networkManager - } + init() {} /// Setup this downloader with rule `JSON` data. /// @@ -494,87 +480,6 @@ public class DebouncingResourceDownloader { let rules = try jsonDecoder.decode([Result].self, from: ruleData) matcher = Matcher(rules: rules) } - - /// Downloads the required resources if they are not available. Loads any cached data if it already exists. - public func startLoading() { - let now = Date() - let resourceURL = self.resourceURL - let cacheFileName = [self.cacheFileName, "json"].joined(separator: ".") - let etagFileName = [cacheFileName, "etag"].joined(separator: ".") - let cacheFolderName = self.cacheFolderName - - guard let cacheFolderURL = FileManager.default.getOrCreateFolder(name: cacheFolderName) else { - Logger.module.error("Failed to get folder: \(cacheFolderName)") - return - } - - if initialLoad { - initialLoad = false - - do { - // Load data from disk if we have it - if let cachedData = try self.dataFromDocument(inFolder: cacheFolderURL, fileName: cacheFileName) { - try setup(withRulesJSON: cachedData) - } - } catch { - Logger.module.error("\(error.localizedDescription)") - } - } - - if now.timeIntervalSince(lastFetchDate) >= fetchInterval { - lastFetchDate = now - - let networkManager = self.networkManager - let etag: String? - let customHeaders: [String: String] - - if let servicesKeyValue = Bundle.main.getPlistString(for: self.servicesKeyName) { - customHeaders = [self.servicesKeyHeaderValue: servicesKeyValue] - } else { - customHeaders = [:] - } - - do { - etag = try self.stringFromDocument(inFolder: cacheFolderURL, fileName: etagFileName) - } catch { - etag = nil - Logger.module.error("\(error.localizedDescription)") - } - - Task { [weak self] in - guard let self = self else { return } - - let resource = try await networkManager.downloadResource( - with: resourceURL, - resourceType: .cached(etag: etag), - checkLastServerSideModification: !AppConstants.buildChannel.isPublic, - customHeaders: customHeaders - ) - - guard !resource.data.isEmpty else { - return - } - - do { - // Save the data to file - try self.writeDataToDisk( - data: resource.data, inFolder: cacheFolderURL, fileName: cacheFileName - ) - - // Save the etag to file - if let data = resource.etag?.data(using: .utf8) { - try self.writeDataToDisk( - data: data, inFolder: cacheFolderURL, fileName: etagFileName - ) - } - - try setup(withRulesJSON: resource.data) - } catch { - Logger.module.error("\(error.localizedDescription)") - } - } - } - } /// Get a possible redirect url for the given URL. Does this /// @@ -583,40 +488,6 @@ public class DebouncingResourceDownloader { func redirectChain(for url: URL) -> [(url: URL, rule: RedirectRule)] { return matcher?.redirectChain(from: url) ?? [] } - - /// Load data from disk given by the folderName and fileName - /// and found in the `applicationSupportDirectory` `SearchPathDirectory`. - /// - /// - Note: `fileName` must contain the full file name including the extension. - private func dataFromDocument(inFolder folderURL: URL, fileName: String) throws -> Data? { - let fileUrl = folderURL.appendingPathComponent(fileName) - return FileManager.default.contents(atPath: fileUrl.path) - } - - /// Load string from the document given by the `folderName` and `fileName` - /// and found in the `applicationSupportDirectory` `SearchPathDirectory`. - /// - /// - Note: `fileName` must contain the full file name including the extension. - private func stringFromDocument(inFolder folderURL: URL, fileName: String) throws -> String? { - let fileUrl = folderURL.appendingPathComponent(fileName) - guard let data = FileManager.default.contents(atPath: fileUrl.path) else { return nil } - return String(data: data, encoding: .utf8) - } - - /// Write data to disk to a file given by `folderName` and `fileName` - /// into the `applicationSupportDirectory` `SearchPathDirectory`. - /// - /// - Note: `fileName` must contain the full file name including the extension. - @discardableResult - func writeDataToDisk(data: Data, inFolder folderURL: URL, fileName: String) throws -> URL { - let fileUrl = folderURL.appendingPathComponent(fileName) - try data.write(to: fileUrl, options: [.atomic]) - return fileUrl - } -} - -enum DirectoryError: Error { - case cannotFindSearchPathDirectory } extension URL { diff --git a/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift b/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift index e60cc4d76d9..d702fdb5f95 100644 --- a/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift +++ b/Sources/Brave/WebFilters/FilterListCustomURLDownloader.swift @@ -9,6 +9,23 @@ import Combine import BraveCore actor FilterListCustomURLDownloader: ObservableObject { + struct DownloadResource: Hashable, DownloadResourceInterface { + let uuid: String + let externalURL: URL + + var cacheFolderName: String { + return ["custom-filter-lists", uuid].joined(separator: "/") + } + + var cacheFileName: String { + return externalURL.lastPathComponent + } + + var headers: [String: String] { + return [:] + } + } + /// A formatter that is used to format a version number private let fileVersionDateFormatter: DateFormatter = { let dateFormatter = DateFormatter() @@ -21,9 +38,9 @@ actor FilterListCustomURLDownloader: ObservableObject { static let shared = FilterListCustomURLDownloader() /// The resource downloader that downloads our resources - private let resourceDownloader: ResourceDownloader + private let resourceDownloader: ResourceDownloader /// Fetch content blocking tasks per filter list - private var fetchTasks: [ResourceDownloader.Resource: Task] + private var fetchTasks: [DownloadResource: Task] init(networkManager: NetworkManager = NetworkManager()) { self.resourceDownloader = ResourceDownloader(networkManager: networkManager) @@ -41,7 +58,7 @@ actor FilterListCustomURLDownloader: ObservableObject { } } - private func handle(downloadResult: ResourceDownloaderStream.DownloadResult, for filterListCustomURL: FilterListCustomURL) async { + private func handle(downloadResult: ResourceDownloaderStream.DownloadResult, for filterListCustomURL: FilterListCustomURL) async { let uuid = await filterListCustomURL.setting.uuid let hasCache = await ContentBlockerManager.shared.hasCache(for: .customFilterList(uuid: uuid)) @@ -158,7 +175,7 @@ actor FilterListCustomURLDownloader: ObservableObject { } extension CustomFilterListSetting { - @MainActor var resource: ResourceDownloader.Resource { - return .customFilterListURL(uuid: uuid, externalURL: externalURL) + @MainActor var resource: FilterListCustomURLDownloader.DownloadResource { + return FilterListCustomURLDownloader.DownloadResource(uuid: uuid, externalURL: externalURL) } } diff --git a/Sources/Brave/WebFilters/FilterListInterface.swift b/Sources/Brave/WebFilters/FilterListInterface.swift index 00bf4ffee44..7bc4d4efb8a 100644 --- a/Sources/Brave/WebFilters/FilterListInterface.swift +++ b/Sources/Brave/WebFilters/FilterListInterface.swift @@ -11,7 +11,7 @@ protocol FilterListInterface { } extension FilterListInterface { - @MainActor func makeResource(componentId: String) -> ResourceDownloader.Resource { + @MainActor func makeResource(componentId: String) -> BraveS3Resource { return .filterListContentBlockingBehaviors( uuid: uuid, componentId: componentId ) diff --git a/Sources/Brave/WebFilters/FilterListResourceDownloader.swift b/Sources/Brave/WebFilters/FilterListResourceDownloader.swift index bd2af6d1504..94be671ed29 100644 --- a/Sources/Brave/WebFilters/FilterListResourceDownloader.swift +++ b/Sources/Brave/WebFilters/FilterListResourceDownloader.swift @@ -107,11 +107,11 @@ public class FilterListResourceDownloader: ObservableObject { /// Manager that handles updates to filter list settings in core data private let settingsManager: FilterListSettingsManager /// The resource downloader that downloads our resources - private let resourceDownloader: ResourceDownloader + private let resourceDownloader: ResourceDownloader /// The filter list subscription private var filterListSubscription: AnyCancellable? /// Fetch content blocking tasks per filter list - private var fetchTasks: [ResourceDownloader.Resource: Task] + private var fetchTasks: [BraveS3Resource: Task] /// Ad block service tasks per filter list UUID private var adBlockServiceTasks: [String: Task] /// A marker that says if fetching has started @@ -359,7 +359,7 @@ public class FilterListResourceDownloader: ObservableObject { /// Start fetching the resource for the given filter list private func startFetchingGenericContentBlockingBehaviors(for filterList: FilterList) { - let resource = ResourceDownloader.Resource.filterListContentBlockingBehaviors( + let resource = BraveS3Resource.filterListContentBlockingBehaviors( uuid: filterList.entry.uuid, componentId: filterList.entry.componentId ) @@ -388,12 +388,12 @@ public class FilterListResourceDownloader: ObservableObject { } /// Cancel all fetching tasks for the given resource - private func stopFetching(resource: ResourceDownloader.Resource) { + private func stopFetching(resource: BraveS3Resource) { fetchTasks[resource]?.cancel() fetchTasks.removeValue(forKey: resource) } - private func handle(downloadResult: ResourceDownloaderStream.DownloadResult, for filterList: FilterListInterface) async { + private func handle(downloadResult: ResourceDownloaderStream.DownloadResult, for filterList: FilterListInterface) async { do { if !downloadResult.isModified { // if the file is not modified first we need to see if we already have a cached value loaded diff --git a/Sources/Brave/WebFilters/ResourceDownloader.swift b/Sources/Brave/WebFilters/ResourceDownloader.swift index cee56b6d2c4..06a1c0dca94 100644 --- a/Sources/Brave/WebFilters/ResourceDownloader.swift +++ b/Sources/Brave/WebFilters/ResourceDownloader.swift @@ -7,108 +7,16 @@ import Foundation import Shared import BraveCore +public protocol DownloadResourceInterface: Sendable { + /// The folder name under which this data should be saved under + var cacheFolderName: String { get } + var cacheFileName: String { get } + var externalURL: URL { get } + var headers: [String: String] { get } +} + /// A ganeric resource downloader class that is responsible for fetching resources -actor ResourceDownloader: Sendable { - enum Resource: Hashable { - /// Rules for debouncing links - case debounceRules - /// Generic filter rules for any locale - case genericFilterRules - /// Generic iOS only content blocking behaviours used for the iOS content blocker - case genericContentBlockingBehaviors - /// Cosmetic filter rules - case generalCosmeticFilters - /// Resources for cosmetic filters - case generalScriptletResources - /// Adblock rules for a filter list - case filterListAdBlockRules(uuid: String, componentId: String) - /// iOS only content blocking behaviours used for the iOS content blocker for a given filter list - case filterListContentBlockingBehaviors(uuid: String, componentId: String) - /// General external file - case dataFile(URL, cacheFolderName: String, cacheFileName: String) - /// Custom filter list URL - case customFilterListURL(uuid: String, externalURL: URL) - - /// The folder name under which this data should be saved under - var cacheFolderName: String { - switch self { - case .debounceRules: - return "debounce-data" - case .filterListContentBlockingBehaviors(_, let componentId), .filterListAdBlockRules(_, let componentId): - return ["filter-lists", componentId].joined(separator: "/") - case .genericFilterRules, .genericContentBlockingBehaviors: - return "abp-data" - case .generalCosmeticFilters, .generalScriptletResources: - return "cmf-data" - case .dataFile(_, let cacheFolderName, _): - return cacheFolderName - case .customFilterListURL(let uuid, _): - return ["custom-filter-lists", uuid].joined(separator: "/") - } - } - - /// The name of the etag save into the cache folder - fileprivate var etagFileName: String { - return [cacheFileName, "etag"].joined(separator: ".") - } - - /// Get the file name that is stored on the device - var cacheFileName: String { - switch self { - case .debounceRules: - return "ios-debouce.json" - case .filterListAdBlockRules(let uuid, _): - return "\(uuid)-latest.txt" - case .filterListContentBlockingBehaviors(let uuid, _): - return "\(uuid)-latest.json" - case .genericFilterRules: - return "latest.txt" - case .genericContentBlockingBehaviors: - return "latest.json" - case .generalCosmeticFilters: - return "ios-cosmetic-filters.dat" - case .generalScriptletResources: - return "scriptlet-resources.json" - case .dataFile(_, _, let cacheFileName): - return cacheFileName - case .customFilterListURL(_, let externalURL): - return externalURL.lastPathComponent - } - } - - /// The base s3 environment url that hosts the debouncing (and other) files. - /// Cannot be used as-is and must be combined with a path - private static var baseResourceURL: URL = { - if AppConstants.buildChannel.isPublic { - return URL(string: "https://adblock-data.s3.brave.com")! - } else { - return URL(string: "https://adblock-data-staging.s3.bravesoftware.com")! - } - }() - - /// Get the external path for the given filter list and this resource type - var externalURL: URL { - switch self { - case .debounceRules: - return Self.baseResourceURL.appendingPathComponent("/ios/debounce.json") - case .filterListContentBlockingBehaviors(let uuid, _): - return Self.baseResourceURL.appendingPathComponent("/ios/\(uuid)-latest.json") - case .filterListAdBlockRules(let uuid, _): - return Self.baseResourceURL.appendingPathComponent("/ios/\(uuid)-latest.txt") - case .genericFilterRules: - return Self.baseResourceURL.appendingPathComponent("/ios/latest.txt") - case .genericContentBlockingBehaviors: - return Self.baseResourceURL.appendingPathComponent("/ios/latest.json") - case .generalCosmeticFilters: - return Self.baseResourceURL.appendingPathComponent("/ios/ios-cosmetic-filters.dat") - case .generalScriptletResources: - return Self.baseResourceURL.appendingPathComponent("/ios/scriptlet-resources.json") - case .dataFile(let url, _, _), .customFilterListURL(_, let url): - return url - } - } - } - +actor ResourceDownloader: Sendable { /// An object representing errors with the resource downloader enum ResourceDownloaderError: Error { case failedToCreateCacheFolder @@ -134,10 +42,6 @@ actor ResourceDownloader: Sendable { return AppConstants.buildChannel.isPublic ? 6.hours : 10.minutes } - /// The name of the info plist key that contains the service key - private static let servicesKeyName = "SERVICES_KEY" - /// The name of the header value that contains the service key - private static let servicesKeyHeaderValue = "BraveServiceKey" /// The netowrk manager performing the requests private let networkManager: NetworkManager @@ -146,7 +50,7 @@ actor ResourceDownloader: Sendable { self.networkManager = networkManager } - func downloadStream(for resource: Resource, every fetchInterval: TimeInterval = defaultFetchInterval) -> ResourceDownloaderStream { + func downloadStream(for resource: Resource, every fetchInterval: TimeInterval = defaultFetchInterval) -> ResourceDownloaderStream { return ResourceDownloaderStream(resource: resource, resourceDownloader: self, fetchInterval: fetchInterval) } @@ -181,12 +85,6 @@ actor ResourceDownloader: Sendable { } private func downloadInternal(resource: Resource) async throws -> DownloadResult { - var headers = [String: String]() - - if let servicesKeyValue = Bundle.main.getPlistString(for: Self.servicesKeyName) { - headers[Self.servicesKeyHeaderValue] = servicesKeyValue - } - let etag = try? Self.etag(for: resource) do { @@ -194,7 +92,7 @@ actor ResourceDownloader: Sendable { with: resource.externalURL, resourceType: .cached(etag: etag), checkLastServerSideModification: !AppConstants.buildChannel.isPublic, - customHeaders: headers) + customHeaders: resource.headers) guard !networkResource.data.isEmpty else { throw DownloadResultError.noData @@ -332,7 +230,7 @@ actor ResourceDownloader: Sendable { #if DEBUG /// Convenience method for tests public static func getMockResponse( - for resource: ResourceDownloader.Resource, + for resource: Resource, statusCode code: Int = 200, headerFields: [String: String]? = nil ) -> HTTPURLResponse { @@ -342,3 +240,10 @@ actor ResourceDownloader: Sendable { } #endif } + +private extension DownloadResourceInterface { + /// The name of the etag save into the cache folder + var etagFileName: String { + return [cacheFileName, "etag"].joined(separator: ".") + } +} diff --git a/Sources/Brave/WebFilters/ResourceDownloaderStream.swift b/Sources/Brave/WebFilters/ResourceDownloaderStream.swift index 531f2bc281f..4fd1ef3256d 100644 --- a/Sources/Brave/WebFilters/ResourceDownloaderStream.swift +++ b/Sources/Brave/WebFilters/ResourceDownloaderStream.swift @@ -6,7 +6,7 @@ import Foundation /// An endless sequence iterator for the given resource -struct ResourceDownloaderStream: Sendable, AsyncSequence, AsyncIteratorProtocol { +struct ResourceDownloaderStream: Sendable, AsyncSequence, AsyncIteratorProtocol { /// An object representing the download struct DownloadResult: Equatable { let date: Date @@ -15,12 +15,12 @@ struct ResourceDownloaderStream: Sendable, AsyncSequence, AsyncIteratorProtocol } typealias Element = Result - private let resource: ResourceDownloader.Resource - private let resourceDownloader: ResourceDownloader + private let resource: Resource + private let resourceDownloader: ResourceDownloader private let fetchInterval: TimeInterval private var firstLoad = true - init(resource: ResourceDownloader.Resource, resourceDownloader: ResourceDownloader, fetchInterval: TimeInterval) { + init(resource: Resource, resourceDownloader: ResourceDownloader, fetchInterval: TimeInterval) { self.resource = resource self.resourceDownloader = resourceDownloader self.fetchInterval = fetchInterval @@ -75,7 +75,7 @@ struct ResourceDownloaderStream: Sendable, AsyncSequence, AsyncIteratorProtocol return self } - static func downloadResult(for resource: ResourceDownloader.Resource) throws -> DownloadResult? { + static func downloadResult(for resource: Resource) throws -> DownloadResult? { guard let fileURL = ResourceDownloader.downloadedFileURL(for: resource) else { return nil } guard let creationDate = try ResourceDownloader.creationDate(for: resource) else { return nil } return DownloadResult(date: creationDate, fileURL: fileURL, isModified: false) diff --git a/Tests/ClientTests/Web Filters/NetworkManager+Tests.swift b/Tests/ClientTests/Web Filters/NetworkManager+Tests.swift index bb65ee99949..933cde56d5c 100644 --- a/Tests/ClientTests/Web Filters/NetworkManager+Tests.swift +++ b/Tests/ClientTests/Web Filters/NetworkManager+Tests.swift @@ -9,7 +9,7 @@ import BraveCore extension NetworkManager { private struct ResourceNotFoundError: Error {} - static func makeNetworkManager(for resources: [ResourceDownloader.Resource], statusCode: Int = 200, etag: String? = nil) -> NetworkManager { + static func makeNetworkManager(for resources: [BraveS3Resource], statusCode: Int = 200, etag: String? = nil) -> NetworkManager { let session = BaseMockNetworkSession { url in guard let resource = resources.first(where: { resource in url.absoluteURL == resource.externalURL @@ -32,7 +32,7 @@ extension NetworkManager { return NetworkManager(session: session) } - static func mockData(for resource: ResourceDownloader.Resource) async throws -> Data { + static func mockData(for resource: BraveS3Resource) async throws -> Data { try await Task.detached(priority: .background) { switch resource { case .debounceRules: diff --git a/Tests/ClientTests/Web Filters/ResourceDownloaderStreamTests.swift b/Tests/ClientTests/Web Filters/ResourceDownloaderStreamTests.swift index df5b480f476..1d580d8d4a0 100644 --- a/Tests/ClientTests/Web Filters/ResourceDownloaderStreamTests.swift +++ b/Tests/ClientTests/Web Filters/ResourceDownloaderStreamTests.swift @@ -11,8 +11,8 @@ class ResourceDownloaderStreamTests: XCTestCase { // Given let expectation = XCTestExpectation(description: "Test downloading resources") expectation.expectedFulfillmentCount = 2 - let resource = ResourceDownloader.Resource.debounceRules - let downloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( + let resource = BraveS3Resource.debounceRules + let downloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( for: [resource], statusCode: 200 )) @@ -33,8 +33,8 @@ class ResourceDownloaderStreamTests: XCTestCase { func testSequenceWithErrorDownload() throws { // Given let expectation = XCTestExpectation(description: "Test downloading resources") - let resource = ResourceDownloader.Resource.debounceRules - let downloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( + let resource = BraveS3Resource.debounceRules + let downloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( for: [resource], statusCode: 404 )) @@ -52,7 +52,7 @@ class ResourceDownloaderStreamTests: XCTestCase { task.cancel() } - @MainActor private func ensureSuccessResult(result: Result, file: StaticString = #filePath, line: UInt = #line) { + @MainActor private func ensureSuccessResult(result: Result.DownloadResult, Error>, file: StaticString = #filePath, line: UInt = #line) { // Then switch result { case .success: @@ -62,7 +62,7 @@ class ResourceDownloaderStreamTests: XCTestCase { } } - @MainActor private func ensureErrorResult(result: Result, file: StaticString = #filePath, line: UInt = #line) { + @MainActor private func ensureErrorResult(result: Result.DownloadResult, Error>, file: StaticString = #filePath, line: UInt = #line) { // Then switch result { case .success: diff --git a/Tests/ClientTests/Web Filters/ResourceDownloaderTests.swift b/Tests/ClientTests/Web Filters/ResourceDownloaderTests.swift index 1af9dc8a931..e84d8d0c9ac 100644 --- a/Tests/ClientTests/Web Filters/ResourceDownloaderTests.swift +++ b/Tests/ClientTests/Web Filters/ResourceDownloaderTests.swift @@ -10,11 +10,11 @@ class ResourceDownloaderTests: XCTestCase { func testSuccessfulResourceDownload() throws { // Given let expectation = XCTestExpectation(description: "Test downloading resources") - let resource = ResourceDownloader.Resource.debounceRules - let firstDownloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( + let resource = BraveS3Resource.debounceRules + let firstDownloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( for: [resource], statusCode: 200, etag: "123" )) - let secondDownloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( + let secondDownloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( for: [resource], statusCode: 304, etag: "123" )) @@ -62,8 +62,8 @@ class ResourceDownloaderTests: XCTestCase { func testFailedResourceDownload() throws { // Given let expectation = XCTestExpectation(description: "Test downloading resource") - let resource = ResourceDownloader.Resource.debounceRules - let downloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( + let resource = BraveS3Resource.debounceRules + let downloader = ResourceDownloader(networkManager: NetworkManager.makeNetworkManager( for: [resource], statusCode: 404 ))