Skip to content

Commit 106f796

Browse files
authored
feat: add space-scoped privacy protections (#243)
* feat(adblock): add AdGuard-backed filter infrastructure * feat(privacy): wire space protections into the browser * refactor(settings): remove privacy tab * fix(privacy): address review feedback
1 parent 4d81174 commit 106f796

20 files changed

Lines changed: 2538 additions & 56 deletions

ora/App/OraRoot.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ struct OraRoot: View {
109109
.enableInjection()
110110
.onAppear {
111111
downloadManager.toastManager = toastManager
112+
Task {
113+
let containerIDs = await MainActor.run {
114+
(try? tabContext.fetch(FetchDescriptor<TabContainer>()))?.map(\.id) ?? []
115+
}
116+
await AdBlockService.shared.start(containerIDs: containerIDs)
117+
}
118+
112119
// Dialog keyboard shortcuts (highest priority — checked first)
113120
keyModifierListener.registerKeyDownHandler { event in
114121
// Escape: dismiss top dialog
@@ -271,6 +278,14 @@ struct OraRoot: View {
271278
}
272279
}
273280

281+
NotificationCenter.default
282+
.addObserver(forName: .spacePrivacySettingsChanged, object: nil, queue: .main) { note in
283+
Task { @MainActor in
284+
guard let containerId = note.userInfo?["containerId"] as? UUID else { return }
285+
tabManager.refreshPrivacySettings(for: containerId)
286+
}
287+
}
288+
274289
// Clear cache and reload
275290
NotificationCenter.default
276291
.addObserver(forName: .clearCacheAndReload, object: nil, queue: .main) { note in

ora/Core/BrowserEngine/BrowserEngine.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ struct BrowserPageConfiguration {
1111
let mediaPlaybackRequiresUserAction: Bool
1212
let scriptMessageNames: [String]
1313
let userScripts: [BrowserUserScript]
14+
let privacySettings: SpacePrivacySettings
1415

15-
static func oraDefault(userScripts: [BrowserUserScript]) -> BrowserPageConfiguration {
16+
static func oraDefault(
17+
userScripts: [BrowserUserScript],
18+
privacySettings: SpacePrivacySettings
19+
) -> BrowserPageConfiguration {
1620
BrowserPageConfiguration(
1721
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0.1 Safari/605.1.15",
1822
allowsPictureInPicture: true,
@@ -23,7 +27,8 @@ struct BrowserPageConfiguration {
2327
allowsBackForwardNavigationGestures: true,
2428
mediaPlaybackRequiresUserAction: false,
2529
scriptMessageNames: ["listener", "linkHover", "mediaEvent", "passwordManager"],
26-
userScripts: userScripts
30+
userScripts: userScripts,
31+
privacySettings: privacySettings
2732
)
2833
}
2934
}

ora/Core/BrowserEngine/BrowserPage.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM
1111
private(set) var lastCommittedURL: URL?
1212
private(set) var isDownloadNavigation = false
1313
private(set) var sslBypassedHosts: Set<String> = []
14+
private var isReadyForNavigation = false
15+
private var pendingLoadRequest: URLRequest?
16+
private var pendingReload = false
1417

1518
init(
1619
profile: BrowserEngineProfile,
@@ -74,6 +77,14 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM
7477
layer.isOpaque = true
7578
layer.drawsAsynchronously = true
7679
}
80+
81+
BrowserPrivacyService.shared.prepareConfiguration(
82+
webConfiguration,
83+
spaceID: profile.identifier
84+
) { [weak self] in
85+
self?.isReadyForNavigation = true
86+
self?.flushPendingNavigationIfNeeded()
87+
}
7788
}
7889

7990
var contentView: NSView {
@@ -109,10 +120,22 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM
109120
}
110121

111122
func load(_ request: URLRequest) {
123+
guard isReadyForNavigation else {
124+
pendingLoadRequest = request
125+
pendingReload = false
126+
return
127+
}
128+
112129
webView.load(request)
113130
}
114131

115132
func reload() {
133+
guard isReadyForNavigation else {
134+
pendingReload = true
135+
pendingLoadRequest = nil
136+
return
137+
}
138+
116139
webView.reload()
117140
}
118141

@@ -164,6 +187,19 @@ final class BrowserPage: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptM
164187
sslBypassedHosts.insert(host)
165188
}
166189

190+
private func flushPendingNavigationIfNeeded() {
191+
if let pendingLoadRequest {
192+
self.pendingLoadRequest = nil
193+
webView.load(pendingLoadRequest)
194+
return
195+
}
196+
197+
if pendingReload {
198+
pendingReload = false
199+
webView.reload()
200+
}
201+
}
202+
167203
private func emitNavigationEvent(
168204
phase: BrowserNavigationPhase,
169205
url: URL?,

ora/Core/Constants/AppEvents.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extension Notification.Name {
2929
// Cache and cookies
3030
static let clearCacheAndReload = Notification.Name("ClearCacheAndReload")
3131
static let clearCookiesAndReload = Notification.Name("ClearCookiesAndReload")
32+
static let spacePrivacySettingsChanged = Notification.Name("SpacePrivacySettingsChanged")
3233

3334
/// App lifecycle
3435
static let quitRequested = Notification.Name("QuitRequested")

ora/Core/Utilities/SettingsStore.swift

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ class SettingsStore: ObservableObject {
147147
private let fingerprintingKey = "settings.tracking.blockFingerprinting"
148148
private let adBlockingKey = "settings.tracking.adBlocking"
149149
private let cookiesPolicyKey = "settings.cookies.policy"
150+
private let adBlockFilterListsKey = "settings.adBlock.filterLists"
150151
private let sitePermissionsKey = "settings.permissions.sitePermissions"
151152
private let customSearchEnginesKey = "settings.customSearchEngines"
152153
private let globalDefaultSearchEngineKey = "settings.globalDefaultSearchEngine"
@@ -176,6 +177,10 @@ class SettingsStore: ObservableObject {
176177
"settings.container.\(containerId.uuidString).autoClearTabsAfter"
177178
}
178179

180+
private func keyForPrivacySettings(for containerId: UUID) -> String {
181+
"settings.container.\(containerId.uuidString).privacy"
182+
}
183+
179184
@Published var autoUpdateEnabled: Bool {
180185
didSet { defaults.set(autoUpdateEnabled, forKey: autoUpdateKey) }
181186
}
@@ -200,6 +205,10 @@ class SettingsStore: ObservableObject {
200205
didSet { saveCodable(sitePermissions, forKey: sitePermissionsKey) }
201206
}
202207

208+
@Published private(set) var adBlockFilterLists: [FilterListRecord] {
209+
didSet { saveCodable(adBlockFilterLists, forKey: adBlockFilterListsKey) }
210+
}
211+
203212
@Published var customSearchEngines: [CustomSearchEngine] {
204213
didSet { saveCodable(customSearchEngines, forKey: customSearchEnginesKey) }
205214
}
@@ -258,7 +267,7 @@ class SettingsStore: ObservableObject {
258267
init() {
259268
autoUpdateEnabled = defaults.bool(forKey: autoUpdateKey)
260269
blockThirdPartyTrackers = defaults.bool(forKey: trackingThirdPartyKey)
261-
blockFingerprinting = defaults.bool(forKey: fingerprintingKey)
270+
blockFingerprinting = defaults.object(forKey: fingerprintingKey) as? Bool ?? true
262271
adBlocking = defaults.bool(forKey: adBlockingKey)
263272
if let raw = defaults.string(forKey: cookiesPolicyKey),
264273
let policy = CookiesPolicy(rawValue: raw)
@@ -271,6 +280,10 @@ class SettingsStore: ObservableObject {
271280
sitePermissions =
272281
Self.loadCodable([String: SitePermissionSettings].self, key: sitePermissionsKey) ?? [:]
273282

283+
adBlockFilterLists = FilterListCatalogService.shared.normalizedRecords(
284+
from: Self.loadCodable([FilterListRecord].self, key: adBlockFilterListsKey) ?? []
285+
)
286+
274287
customSearchEngines =
275288
Self.loadCodable([CustomSearchEngine].self, key: customSearchEnginesKey) ?? []
276289

@@ -358,10 +371,29 @@ class SettingsStore: ObservableObject {
358371
objectWillChange.send()
359372
}
360373

374+
func privacySettings(for containerId: UUID) -> SpacePrivacySettings {
375+
Self.loadCodable(SpacePrivacySettings.self, key: keyForPrivacySettings(for: containerId))
376+
?? legacyPrivacySettings
377+
}
378+
379+
func setPrivacySettings(_ value: SpacePrivacySettings, for containerId: UUID) {
380+
saveCodable(value, forKey: keyForPrivacySettings(for: containerId))
381+
objectWillChange.send()
382+
}
383+
384+
func notifySpacePrivacySettingsChanged(for containerId: UUID) {
385+
NotificationCenter.default.post(
386+
name: .spacePrivacySettingsChanged,
387+
object: nil,
388+
userInfo: ["containerId": containerId]
389+
)
390+
}
391+
361392
func removeContainerSettings(for containerId: UUID) {
362393
defaults.removeObject(forKey: keyForDefaultSearch(for: containerId))
363394
defaults.removeObject(forKey: keyForDefaultAI(for: containerId))
364395
defaults.removeObject(forKey: keyForAutoClear(for: containerId))
396+
defaults.removeObject(forKey: keyForPrivacySettings(for: containerId))
365397
objectWillChange.send()
366398
}
367399

@@ -387,6 +419,32 @@ class SettingsStore: ObservableObject {
387419
customSearchEngines = engines
388420
}
389421

422+
// MARK: - Ad block filter catalog
423+
424+
func adBlockFilterList(id: String) -> FilterListRecord? {
425+
adBlockFilterLists.first { $0.id == id }
426+
}
427+
428+
func setAdBlockFilterLists(_ records: [FilterListRecord]) {
429+
adBlockFilterLists = FilterListCatalogService.shared.normalizedRecords(from: records)
430+
}
431+
432+
func upsertAdBlockFilterList(_ record: FilterListRecord) {
433+
var records = adBlockFilterLists
434+
if let index = records.firstIndex(where: { $0.id == record.id }) {
435+
records[index] = record
436+
} else {
437+
records.append(record)
438+
}
439+
adBlockFilterLists = FilterListCatalogService.shared.normalizedRecords(from: records)
440+
}
441+
442+
func removeAdBlockFilterList(id: String) {
443+
adBlockFilterLists = FilterListCatalogService.shared.normalizedRecords(
444+
from: adBlockFilterLists.filter { $0.id != id }
445+
)
446+
}
447+
390448
func removeCustomSearchEngine(withId id: String) {
391449
customSearchEngines = customSearchEngines.filter { $0.id != id }
392450
}
@@ -459,4 +517,13 @@ class SettingsStore: ObservableObject {
459517
abs(lhs - value) < abs(rhs - value)
460518
} ?? defaultSeconds
461519
}
520+
521+
private var legacyPrivacySettings: SpacePrivacySettings {
522+
SpacePrivacySettings(
523+
blockThirdPartyTrackers: blockThirdPartyTrackers,
524+
blockFingerprinting: blockFingerprinting,
525+
adBlocking: adBlocking,
526+
cookiesPolicy: cookiesPolicy
527+
)
528+
}
462529
}

0 commit comments

Comments
 (0)