This repository has been archived by the owner on May 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 442
/
AdBlockStats.swift
353 lines (312 loc) · 14.1 KB
/
AdBlockStats.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/* 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
import BraveShared
import Combine
import BraveCore
import Data
import os.log
/// This object holds on to our adblock engines and returns information needed for stats tracking as well as some conveniences
/// for injected scripts needed during web navigation and cosmetic filters models needed by the `SelectorsPollerScript.js` script.
public actor AdBlockStats {
typealias CosmeticFilterModelTuple = (isAlwaysAggressive: Bool, model: CosmeticFilterModel)
public static let shared = AdBlockStats()
/// An object containing the basic information to allow us to compile an engine
public struct LazyFilterListInfo {
let filterListInfo: CachedAdBlockEngine.FilterListInfo
let isAlwaysAggressive: Bool
}
/// A list of filter list info that are available for compilation. This information is used for lazy loading.
private(set) var availableFilterLists: [CachedAdBlockEngine.Source: LazyFilterListInfo]
/// The info for the resource file. This is a shared file used by all filter lists that contain scriplets. This information is used for lazy loading.
public private(set) var resourcesInfo: CachedAdBlockEngine.ResourcesInfo?
/// Adblock engine for general adblock lists.
private(set) var cachedEngines: [CachedAdBlockEngine.Source: CachedAdBlockEngine]
/// The current task that is compiling.
private var currentCompileTask: Task<(), Never>?
/// Return all the critical sources
///
/// Critical sources are those that are enabled and are "on" by default. Giving us the most important sources.
/// Used for memory managment so we know which filter lists to disable upon a memory warning
@MainActor var criticalSources: [CachedAdBlockEngine.Source] {
var enabledSources: [CachedAdBlockEngine.Source] = [.adBlock]
enabledSources.append(contentsOf: FilterListStorage.shared.criticalSources)
return enabledSources
}
/// Return an array of all sources that are enabled according to user's settings
/// - Note: This does not take into account the domain or global adblock toggle
@MainActor var enabledSources: [CachedAdBlockEngine.Source] {
var enabledSources: [CachedAdBlockEngine.Source] = [.adBlock]
enabledSources.append(contentsOf: FilterListStorage.shared.enabledSources)
enabledSources.append(contentsOf: CustomFilterListStorage.shared.enabledSources)
return enabledSources
}
init() {
cachedEngines = [:]
availableFilterLists = [:]
}
/// Handle memory warnings by freeing up some memory
func didReceiveMemoryWarning() async {
cachedEngines.values.forEach({ $0.clearCaches() })
await removeDisabledEngines()
}
/// Create and add an engine from the given resources.
/// If an engine already exists for the given source, it will be replaced.
///
/// - Note: This method will ensure syncronous compilation
public func compile(
lazyInfo: LazyFilterListInfo, resourcesInfo: CachedAdBlockEngine.ResourcesInfo,
compileContentBlockers: Bool
) async {
await currentCompileTask?.value
currentCompileTask = Task {
// Compile engine
if needsCompilation(for: lazyInfo.filterListInfo, resourcesInfo: resourcesInfo) {
do {
let engine = try CachedAdBlockEngine.compile(
filterListInfo: lazyInfo.filterListInfo, resourcesInfo: resourcesInfo, isAlwaysAggressive: lazyInfo.isAlwaysAggressive
)
add(engine: engine)
} catch {
ContentBlockerManager.log.error("Failed to compile engine for \(lazyInfo.filterListInfo.source.debugDescription)")
}
}
// Compile content blockers
if compileContentBlockers, let blocklistType = lazyInfo.blocklistType {
let modes = await ContentBlockerManager.shared.missingModes(for: blocklistType)
guard !modes.isEmpty else { return }
do {
try await ContentBlockerManager.shared.compileRuleList(
at: lazyInfo.filterListInfo.localFileURL, for: blocklistType, modes: modes
)
} catch {
ContentBlockerManager.log.error("Failed to compile rule list for \(lazyInfo.filterListInfo.source.debugDescription)")
}
}
}
await currentCompileTask?.value
}
/// Add a new engine to the list.
/// If an engine already exists for the same source, it will be replaced instead.
private func add(engine: CachedAdBlockEngine) {
cachedEngines[engine.filterListInfo.source] = engine
updateIfNeeded(resourcesInfo: engine.resourcesInfo)
updateIfNeeded(filterListInfo: engine.filterListInfo, isAlwaysAggressive: engine.isAlwaysAggressive)
ContentBlockerManager.log.debug("Added engine for \(engine.filterListInfo.debugDescription)")
}
/// Add or update `filterListInfo` if it is a newer version. This information is used for lazy loading.
func updateIfNeeded(filterListInfo: CachedAdBlockEngine.FilterListInfo, isAlwaysAggressive: Bool) {
if let existingLazyInfo = availableFilterLists[filterListInfo.source] {
guard filterListInfo.version > existingLazyInfo.filterListInfo.version else { return }
}
availableFilterLists[filterListInfo.source] = LazyFilterListInfo(
filterListInfo: filterListInfo, isAlwaysAggressive: isAlwaysAggressive
)
}
/// Add or update `resourcesInfo` if it is a newer version. This information is used for lazy loading.
func updateIfNeeded(resourcesInfo: CachedAdBlockEngine.ResourcesInfo) {
guard self.resourcesInfo == nil || resourcesInfo.version > self.resourcesInfo!.version else { return }
self.resourcesInfo = resourcesInfo
}
/// Remove all the engines
func removeAllEngines() {
cachedEngines.removeAll()
}
/// Remove all engines that have disabled sources
func removeDisabledEngines() async {
let sources = await Set(enabledSources)
for source in cachedEngines.keys {
guard !sources.contains(source) else { continue }
// Remove the engine
if let filterListInfo = cachedEngines[source]?.filterListInfo {
cachedEngines.removeValue(forKey: source)
ContentBlockerManager.log.debug("Removed engine for \(filterListInfo.debugDescription)")
}
// Delete the Content blockers
if let lazyInfo = availableFilterLists[source], let blocklistType = lazyInfo.blocklistType {
do {
try await ContentBlockerManager.shared.removeRuleLists(for: blocklistType)
} catch {
ContentBlockerManager.log.error("Failed to remove rule lists for \(lazyInfo.filterListInfo.debugDescription)")
}
}
}
}
/// Remove all engines that have disabled sources
func ensureEnabledEngines() async {
do {
for source in await enabledSources {
guard cachedEngines[source] == nil else { continue }
guard let availableFilterList = availableFilterLists[source] else { continue }
guard let resourcesInfo = self.resourcesInfo else { continue }
await compile(
lazyInfo: availableFilterList,
resourcesInfo: resourcesInfo,
compileContentBlockers: true
)
// Sleep for 1ms. This drastically reduces memory usage without much impact to usability
try await Task.sleep(nanoseconds: 1000000)
}
} catch {
// Ignore cancellation errors
}
}
/// Tells us if this source should be eagerly loaded.
///
/// Eagerness is determined by several factors:
/// * If the source represents a fitler list or a custom filter list, it is eager if it is enabled
/// * If the source represents the `adblock` default filter list, it is always eager regardless of shield settings
@MainActor func isEagerlyLoaded(source: CachedAdBlockEngine.Source) -> Bool {
return enabledSources.contains(source)
}
/// Tells us if an engine needs compilation if it's missing or if its resources are outdated
func needsCompilation(for filterListInfo: CachedAdBlockEngine.FilterListInfo, resourcesInfo: CachedAdBlockEngine.ResourcesInfo) -> Bool {
if let cachedEngine = cachedEngines[filterListInfo.source] {
return cachedEngine.filterListInfo.version < filterListInfo.version
&& cachedEngine.resourcesInfo.version < resourcesInfo.version
} else {
return true
}
}
/// Checks the general and regional engines to see if the request should be blocked
func shouldBlock(requestURL: URL, sourceURL: URL, resourceType: AdblockEngine.ResourceType, isAggressiveMode: Bool) async -> Bool {
let sources = await self.enabledSources
return await cachedEngines(for: sources).asyncConcurrentMap({ cachedEngine in
return await cachedEngine.shouldBlock(
requestURL: requestURL,
sourceURL: sourceURL,
resourceType: resourceType,
isAggressiveMode: isAggressiveMode
)
}).contains(where: { $0 })
}
/// This returns all the user script types for the given frame
func makeEngineScriptTypes(frameURL: URL, isMainFrame: Bool, domain: Domain) async -> Set<UserScriptType> {
// Add any engine scripts for this frame
return await cachedEngines(for: domain).enumerated().asyncMap({ index, cachedEngine -> Set<UserScriptType> in
do {
return try await cachedEngine.makeEngineScriptTypes(
frameURL: frameURL, isMainFrame: isMainFrame, domain: domain, index: index
)
} catch {
assertionFailure()
return []
}
}).reduce(Set<UserScriptType>(), { partialResult, scriptTypes in
return partialResult.union(scriptTypes)
})
}
/// Returns all appropriate engines for the given domain
@MainActor func cachedEngines(for domain: Domain) async -> [CachedAdBlockEngine] {
let sources = enabledSources(for: domain)
return await cachedEngines(for: sources)
}
/// Return all the cached engines for the given sources. If any filter list is not yet loaded, it will be lazily loaded
private func cachedEngines(for sources: [CachedAdBlockEngine.Source]) -> [CachedAdBlockEngine] {
return sources.compactMap { source -> CachedAdBlockEngine? in
return cachedEngines[source]
}
}
/// Returns all the models for this frame URL
func cosmeticFilterModels(forFrameURL frameURL: URL, domain: Domain) async -> [CosmeticFilterModelTuple] {
return await cachedEngines(for: domain).asyncConcurrentCompactMap { cachedEngine -> CosmeticFilterModelTuple? in
do {
guard let model = try await cachedEngine.cosmeticFilterModel(forFrameURL: frameURL) else {
return nil
}
return (cachedEngine.isAlwaysAggressive, model)
} catch {
assertionFailure()
return nil
}
}
}
/// Give us all the enabled sources for the given domain
@MainActor private func enabledSources(for domain: Domain) -> [CachedAdBlockEngine.Source] {
let enabledSources = self.enabledSources
return enabledSources.filter({ $0.isEnabled(for: domain )})
}
}
extension FilterListSetting {
@MainActor var engineSource: CachedAdBlockEngine.Source? {
guard let componentId = componentId else { return nil }
return .filterList(componentId: componentId)
}
}
extension FilterList {
@MainActor var engineSource: CachedAdBlockEngine.Source {
return .filterList(componentId: entry.componentId)
}
}
private extension FilterListStorage {
/// Gives us source representations of all the critical filter lists
///
/// Critical filter lists are those that are enabled and are "on" by default. Giving us the most important filter lists.
/// Used for memory managment so we know which filter lists to disable upon a memory warning
@MainActor var criticalSources: [CachedAdBlockEngine.Source] {
return enabledSources.filter { source in
switch source {
case .filterList(let componentId):
return FilterList.defaultOnComponentIds.contains(componentId)
default:
return false
}
}
}
/// Gives us source representations of all the enabled filter lists
@MainActor var enabledSources: [CachedAdBlockEngine.Source] {
if !filterLists.isEmpty {
return filterLists.compactMap { filterList -> CachedAdBlockEngine.Source? in
guard filterList.isEnabled else { return nil }
return filterList.engineSource
}
} else {
// We may not have the filter lists loaded yet. In which case we load the settings
return allFilterListSettings.compactMap { setting -> CachedAdBlockEngine.Source? in
guard setting.isEnabled else { return nil }
return setting.engineSource
}
}
}
}
extension CustomFilterListSetting {
@MainActor var engineSource: CachedAdBlockEngine.Source {
return .filterListURL(uuid: uuid)
}
}
private extension CustomFilterListStorage {
/// Gives us source representations of all the enabled custom filter lists
@MainActor var enabledSources: [CachedAdBlockEngine.Source] {
return filterListsURLs.compactMap { filterList -> CachedAdBlockEngine.Source? in
guard filterList.setting.isEnabled else { return nil }
return filterList.setting.engineSource
}
}
}
private extension CachedAdBlockEngine.Source {
/// Returns a boolean indicating if the engine is enabled for the given domain.
///
/// This is determined by checking the source of the engine and checking the appropriate shields.
@MainActor func isEnabled(for domain: Domain) -> Bool {
switch self {
case .adBlock, .filterList, .filterListURL:
// This engine source type is enabled only if shields are enabled
// for the given domain
return domain.isShieldExpected(.AdblockAndTp, considerAllShieldsOption: true)
}
}
}
extension AdBlockStats.LazyFilterListInfo {
var blocklistType: ContentBlockerManager.BlocklistType? {
switch filterListInfo.source {
case .adBlock:
// Normally this should be .generic(.blockAds)
// but this content blocker is coming from slim-list
return nil
case .filterList(let componentId):
return .filterList(componentId: componentId, isAlwaysAggressive: isAlwaysAggressive)
case .filterListURL(let uuid):
return .customFilterList(uuid: uuid)
}
}
}