From 39f685a4325b65aab822b8550a32c2832bd9bf7e Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 12 Jan 2022 12:49:01 +0700 Subject: [PATCH 1/9] Popup windows design review fixes (#368) * fix opening Popup Tabs in new Tabs * Bump privacy dashboard to latest version * Popup windows fixes * lost file * Fix deallocation * Check persisted popup permission against main frame URL instead of source frame URL * PFR fixes * added TabDelegate.tabWillStartNavigation: activate popup on user initiated navigation * Hide Dax button on Popups * Display opener URL for about:blank Popups * fix tests Co-authored-by: Alistair Brown --- DuckDuckGo/BrowserTab/Model/Tab.swift | 17 +++- .../View/BrowserTabViewController.swift | 19 +++-- .../BrowserTab/ViewModel/TabViewModel.swift | 14 ++-- DuckDuckGo/Common/Localizables/UserText.swift | 1 + .../Main/View/MainWindowController.swift | 9 ++- .../AddressBarButtonsViewController.swift | 30 ++++--- .../View/AddressBarViewController.swift | 13 +++- .../View/Base.lproj/NavigationBar.storyboard | 2 + .../Permissions/Model/PermissionManager.swift | 40 ++++------ .../Permissions/Model/PermissionModel.swift | 62 ++++++++------- .../Permissions/Model/PermissionStore.swift | 27 +++---- .../Permissions/Model/PermissionType.swift | 8 ++ .../Permissions.xcdatamodel/contents | 5 +- .../Permissions/Model/StoredPermission.swift | 49 +++++++++++- .../View/PermissionContextMenu.swift | 19 +++-- .../Model/PermissionAuthorizationState.swift | 20 +++++ .../Model/PrivacyDashboardUserScript.swift | 13 +++- .../View/PrivacyDashboardViewController.swift | 18 ++--- DuckDuckGo/Windows/View/WindowsManager.swift | 21 +++-- Submodules/duckduckgo-privacy-dashboard | 2 +- .../Permissions/PermissionManagerMock.swift | 9 ++- .../Permissions/PermissionManagerTests.swift | 78 +++++++++---------- .../Permissions/PermissionModelTests.swift | 20 ++--- .../Permissions/PermissionStoreMock.swift | 14 ++-- .../Permissions/PermissionStoreTests.swift | 34 ++++---- 25 files changed, 345 insertions(+), 199 deletions(-) diff --git a/DuckDuckGo/BrowserTab/Model/Tab.swift b/DuckDuckGo/BrowserTab/Model/Tab.swift index dee7619c51..80d12d46b7 100644 --- a/DuckDuckGo/BrowserTab/Model/Tab.swift +++ b/DuckDuckGo/BrowserTab/Model/Tab.swift @@ -23,6 +23,7 @@ import Combine import BrowserServicesKit protocol TabDelegate: FileDownloadManagerDelegate { + func tabWillStartNavigation(_ tab: Tab, isUserInitiated: Bool) func tabDidStartNavigation(_ tab: Tab) func tab(_ tab: Tab, requestedNewTabWith content: Tab.TabContent, selected: Bool) func tab(_ tab: Tab, willShowContextMenuAt position: NSPoint, image: URL?, link: URL?, selectedText: String?) @@ -733,6 +734,7 @@ extension Tab: WKNavigationDelegate { } guard let url = navigationAction.request.url, let urlScheme = url.scheme else { + self.willPerformNavigationAction(navigationAction) decisionHandler(.allow) return } @@ -757,8 +759,12 @@ extension Tab: WKNavigationDelegate { } HTTPSUpgrade.shared.isUpgradeable(url: url) { [weak self] isUpgradable in - if let self = self, - isUpgradable && navigationAction.isTargetingMainFrame, + guard let self = self else { + decisionHandler(.cancel) + return + } + + if isUpgradable && navigationAction.isTargetingMainFrame, let upgradedUrl = url.toHttps() { self.invalidateBackItemIfNeeded(for: navigationAction) @@ -768,12 +774,19 @@ extension Tab: WKNavigationDelegate { return } + self.willPerformNavigationAction(navigationAction) decisionHandler(.allow) } } // swiftlint:enable cyclomatic_complexity // swiftlint:enable function_body_length + private func willPerformNavigationAction(_ navigationAction: WKNavigationAction) { + if navigationAction.isTargetingMainFrame { + delegate?.tabWillStartNavigation(self, isUserInitiated: navigationAction.isUserInitiated) + } + } + private func invalidateBackItemIfNeeded(for navigationAction: WKNavigationAction) { guard let url = navigationAction.request.url, url == self.clientRedirectedDuringNavigationURL diff --git a/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift b/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift index ba4bf15614..d69b3ce234 100644 --- a/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/BrowserTab/View/BrowserTabViewController.swift @@ -306,6 +306,16 @@ final class BrowserTabViewController: NSViewController { extension BrowserTabViewController: TabDelegate { + func tabWillStartNavigation(_ tab: Tab, isUserInitiated: Bool) { + if isUserInitiated, + let window = self.view.window, + window.isPopUpWindow == true, + window.isKeyWindow == false { + + window.makeKeyAndOrderFront(nil) + } + } + func tab(_ tab: Tab, requestedOpenExternalURL url: URL, forUserEnteredURL userEntered: Bool) { guard let window = self.view.window else { os_log("%s: Window is nil", type: .error, className) @@ -640,12 +650,11 @@ extension BrowserTabViewController: WKUIDelegate { parentTab.permissions.authorizationQueries.first(where: { $0.permissions.contains(.popups) }) } + let contentSize = NSSize(width: windowFeatures.width?.intValue ?? 1024, height: windowFeatures.height?.intValue ?? 752) var shouldOpenPopUp = navigationAction.isUserInitiated if !shouldOpenPopUp { let url = navigationAction.request.url - parentTab.permissions.permissions([.popups], - requestedForDomain: navigationAction.sourceFrame.request.url?.host, - url: url) { [weak parentTab] granted in + parentTab.permissions.permissions([.popups], requestedForDomain: webView.url?.host, url: url) { [weak parentTab] granted in guard let parentTab = parentTab else { return } @@ -657,7 +666,7 @@ extension BrowserTabViewController: WKUIDelegate { // called asynchronously guard let url = navigationAction.request.url else { return } let tab = makeTab(parentTab: parentTab, content: .url(url)) - WindowsManager.openPopUpWindow(with: tab) + WindowsManager.openPopUpWindow(with: tab, contentSize: contentSize) parentTab.permissions.permissions.popups.popupOpened(nextQuery: nextQuery(parentTab: parentTab)) @@ -675,7 +684,7 @@ extension BrowserTabViewController: WKUIDelegate { if windowFeatures.toolbarsVisibility?.boolValue == true { tabCollectionViewModel.insertChild(tab: tab, selected: true) } else { - WindowsManager.openPopUpWindow(with: tab) + WindowsManager.openPopUpWindow(with: tab, contentSize: contentSize) } parentTab.permissions.permissions.popups.popupOpened(nextQuery: nextQuery(parentTab: parentTab)) diff --git a/DuckDuckGo/BrowserTab/ViewModel/TabViewModel.swift b/DuckDuckGo/BrowserTab/ViewModel/TabViewModel.swift index da393f38d7..8b37490e8e 100644 --- a/DuckDuckGo/BrowserTab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/BrowserTab/ViewModel/TabViewModel.swift @@ -146,7 +146,7 @@ final class TabViewModel { return } - guard let url = tab.content.url else { + guard let url = tab.content.url ?? tab.parentTab?.content.url else { addressBarString = "" passiveAddressBarString = "" return @@ -164,19 +164,15 @@ final class TabViewModel { return } - guard let host = url.host else { + guard let host = url.host ?? tab.parentTab?.content.url?.host else { + // also lands here for about:blank and about:home addressBarString = "" passiveAddressBarString = "" return } - if [.blankPage, .homePage].contains(url) { - addressBarString = "" - passiveAddressBarString = "" - } else { - addressBarString = url.absoluteString - passiveAddressBarString = host.drop(prefix: URL.HostPrefix.www.separated()) - } + addressBarString = url.absoluteString + passiveAddressBarString = host.drop(prefix: URL.HostPrefix.www.separated()) } private func updateTitle() { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 7aa5a7aeb9..5553a501ed 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -188,6 +188,7 @@ struct UserText { static let permissionAlwaysDenyFormat = NSLocalizedString("permission.always.deny.dashboard", value: "Always Deny on “%@“", comment: "Make input media device access permanently disabled for current domain (Option in Privacy Dashboard)") static let permissionAlwaysAllowDeviceFormat = NSLocalizedString("permission.always.allow", value: "Always Allow %@ on “%@“", comment: "Make input media device access permanently allowed for current domain") + static let permissionAlwaysAllowPopupsFormat = NSLocalizedString("permission.always.allow.popups", value: "Always Allow on “%@“", comment: "Make popups permanently allowed for current domain") static let permissionAlwaysAskDeviceFormat = NSLocalizedString("permission.always.ask", value: "Always Ask for %@ on “%@“", comment: "Make input media device access always asked from user for current domain") static let permissionAlwaysDenyDeviceFormat = NSLocalizedString("permission.always.deny.device", value: "Never Ask for %@ again for “%@“", comment: "Make input media device access permanently allowed for current domain") diff --git a/DuckDuckGo/Main/View/MainWindowController.swift b/DuckDuckGo/Main/View/MainWindowController.swift index ee5e826af3..2b96bf3293 100644 --- a/DuckDuckGo/Main/View/MainWindowController.swift +++ b/DuckDuckGo/Main/View/MainWindowController.swift @@ -37,7 +37,14 @@ final class MainWindowController: NSWindowController { init(mainViewController: MainViewController, popUp: Bool, fireViewModel: FireViewModel = FireCoordinator.fireViewModel) { let makeWindow: (NSRect) -> NSWindow = popUp ? PopUpWindow.init(frame:) : MainWindow.init(frame:) - let window = makeWindow(NSRect(x: 0, y: 0, width: 1024, height: 790)) + + let size = mainViewController.view.frame.size + let moveToCenter = CGAffineTransform(translationX: ((NSScreen.main?.frame.width ?? 1024) - size.width) / 2, + y: ((NSScreen.main?.frame.height ?? 790) - size.height) / 2) + let frame = NSRect(origin: (NSScreen.main?.frame.origin ?? .zero).applying(moveToCenter), + size: size) + + let window = makeWindow(frame) window.contentViewController = mainViewController self.fireViewModel = fireViewModel diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index a0202f137a..e7ee41c37e 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -78,6 +78,8 @@ final class AddressBarButtonsViewController: NSViewController { @IBOutlet weak var imageButton: NSButton! @IBOutlet weak var clearButton: NSButton! + @IBOutlet weak var buttonsContainer: NSStackView! + @IBOutlet weak var animationWrapperView: NSView! var trackerAnimationView1: AnimationView! var trackerAnimationView2: AnimationView! @@ -115,6 +117,8 @@ final class AddressBarButtonsViewController: NSViewController { } } + @Published private(set) var buttonsWidth: CGFloat = 0 + private var tabCollectionViewModel: TabCollectionViewModel private var bookmarkManager: BookmarkManager = LocalBookmarkManager.shared var controllerMode: AddressBarViewController.Mode? { @@ -159,17 +163,15 @@ final class AddressBarButtonsViewController: NSViewController { super.viewDidLoad() setupAnimationViews() - setupButtons() subscribeToSelectedTabViewModel() subscribeToBookmarkList() subscribePrivacyDashboardPendingUpdates() subscribeToEffectiveAppearance() updateBookmarkButtonVisibility() + } - cameraButton.sendAction(on: .leftMouseDown) - microphoneButton.sendAction(on: .leftMouseDown) - geolocationButton.sendAction(on: .leftMouseDown) - popupsButton.sendAction(on: .leftMouseDown) + override func viewWillAppear() { + setupButtons() } var mouseEnterExitTrackingArea: NSTrackingArea? @@ -179,6 +181,7 @@ final class AddressBarButtonsViewController: NSViewController { if view.window?.isPopUpWindow == false { updateTrackingAreaForHover() } + self.buttonsWidth = buttonsContainer.frame.size.width + 4.0 } func updateTrackingAreaForHover() { @@ -225,6 +228,8 @@ final class AddressBarButtonsViewController: NSViewController { } private func updateBookmarkButtonVisibility() { + guard view.window?.isPopUpWindow == false else { return } + let hasEmptyAddressBar = tabCollectionViewModel.selectedTabViewModel?.addressBarString.isEmpty ?? true let showBookmarkButton = clearButton.isHidden && !hasEmptyAddressBar && (isMouseOver || bookmarkPopover.isShown) @@ -402,6 +407,11 @@ final class AddressBarButtonsViewController: NSViewController { privacyEntryPointButton.contentTintColor = .privacyEnabledColor imageButton.applyFaviconStyle() + + cameraButton.sendAction(on: .leftMouseDown) + microphoneButton.sendAction(on: .leftMouseDown) + geolocationButton.sendAction(on: .leftMouseDown) + popupsButton.sendAction(on: .leftMouseDown) } private var animationViewCache = [String: AnimationView]() @@ -612,7 +622,9 @@ final class AddressBarButtonsViewController: NSViewController { !isHypertextUrl || selectedTabViewModel.errorViewState.isVisible || isTextFieldValueText - imageButtonWrapper.isHidden = !privacyEntryPointButton.isHidden || isAnyTrackerAnimationPlaying + imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true + || !privacyEntryPointButton.isHidden + || isAnyTrackerAnimationPlaying } private func updatePrivacyEntryPointIcon() { @@ -776,13 +788,13 @@ extension AddressBarButtonsViewController: PermissionContextMenuDelegate { tabCollectionViewModel.selectedTabViewModel?.tab.permissions.allow(query) } func permissionContextMenu(_ menu: PermissionContextMenu, alwaysAllowPermission permission: PermissionType) { - PermissionManager.shared.setPermission(true, forDomain: menu.domain, permissionType: permission) + PermissionManager.shared.setPermission(.allow, forDomain: menu.domain, permissionType: permission) } func permissionContextMenu(_ menu: PermissionContextMenu, alwaysDenyPermission permission: PermissionType) { - PermissionManager.shared.setPermission(false, forDomain: menu.domain, permissionType: permission) + PermissionManager.shared.setPermission(.deny, forDomain: menu.domain, permissionType: permission) } func permissionContextMenu(_ menu: PermissionContextMenu, resetStoredPermission permission: PermissionType) { - PermissionManager.shared.removePermission(forDomain: menu.domain, permissionType: permission) + PermissionManager.shared.setPermission(.ask, forDomain: menu.domain, permissionType: permission) } func permissionContextMenuReloadPage(_ menu: PermissionContextMenu) { tabCollectionViewModel.selectedTabViewModel?.tab.reload() diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 53b972516d..503e095709 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -28,6 +28,7 @@ final class AddressBarViewController: NSViewController { @IBOutlet var activeBackgroundView: NSView! @IBOutlet var activeBackgroundViewWithSuggestions: NSView! @IBOutlet var progressIndicator: ProgressView! + @IBOutlet var passiveTextFieldMinXConstraint: NSLayoutConstraint! private(set) var addressBarButtonsViewController: AddressBarButtonsViewController? @@ -56,7 +57,7 @@ final class AddressBarViewController: NSViewController { } } - private var selectedTabViewModelCancellable: AnyCancellable? + private var cancellables = Set() private var passiveAddressBarStringCancellable: AnyCancellable? private var suggestionsVisibleCancellable: AnyCancellable? private var frameCancellable: AnyCancellable? @@ -120,6 +121,7 @@ final class AddressBarViewController: NSViewController { object: nil) addMouseMonitors() } + subscribeToButtonsWidth() } // swiftlint:disable notification_center_detachment @@ -164,12 +166,12 @@ final class AddressBarViewController: NSViewController { @IBOutlet var shadowView: ShadowView! private func subscribeToSelectedTabViewModel() { - selectedTabViewModelCancellable = tabCollectionViewModel.$selectedTabViewModel.receive(on: DispatchQueue.main).sink { [weak self] _ in + tabCollectionViewModel.$selectedTabViewModel.receive(on: DispatchQueue.main).sink { [weak self] _ in self?.subscribeToPassiveAddressBarString() self?.subscribeToProgressEvents() // don't resign first responder on tab switching self?.clickPoint = nil - } + }.store(in: &cancellables) } private func subscribeToPassiveAddressBarString() { @@ -223,6 +225,11 @@ final class AddressBarViewController: NSViewController { } } + func subscribeToButtonsWidth() { + addressBarButtonsViewController!.$buttonsWidth.weakAssign(to: \.constant, on: passiveTextFieldMinXConstraint) + .store(in: &cancellables) + } + private func updateView() { let isPassiveTextFieldHidden = isFirstResponder || mode.isEditing addressBarTextField.alphaValue = isPassiveTextFieldHidden ? 1 : 0 diff --git a/DuckDuckGo/NavigationBar/View/Base.lproj/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/Base.lproj/NavigationBar.storyboard index 1ae8f8c35f..dd60841ea3 100644 --- a/DuckDuckGo/NavigationBar/View/Base.lproj/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/Base.lproj/NavigationBar.storyboard @@ -448,6 +448,7 @@ + @@ -716,6 +717,7 @@ + diff --git a/DuckDuckGo/Permissions/Model/PermissionManager.swift b/DuckDuckGo/Permissions/Model/PermissionManager.swift index 4c7c1d9775..0083b761ee 100644 --- a/DuckDuckGo/Permissions/Model/PermissionManager.swift +++ b/DuckDuckGo/Permissions/Model/PermissionManager.swift @@ -22,12 +22,11 @@ import os.log protocol PermissionManagerProtocol: AnyObject { - typealias PublishedPermission = (domain: String, permissionType: PermissionType, grant: Bool?) + typealias PublishedPermission = (domain: String, permissionType: PermissionType, decision: PersistedPermissionDecision) var permissionPublisher: AnyPublisher { get } - func permission(forDomain domain: String, permissionType: PermissionType) -> Bool? - func setPermission(_ permission: Bool, forDomain domain: String, permissionType: PermissionType) - func removePermission(forDomain domain: String, permissionType: PermissionType) + func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision + func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) func burnPermissions(except fireproofDomains: FireproofDomains, completion: @escaping () -> Void) func burnPermissions(of domains: Set, completion: @escaping () -> Void) @@ -60,26 +59,31 @@ final class PermissionManager: PermissionManagerProtocol { } } - func permission(forDomain domain: String, permissionType: PermissionType) -> Bool? { - return permissions[domain.dropWWW()]?[permissionType]?.allow + func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision { + return permissions[domain.dropWWW()]?[permissionType]?.decision ?? .ask } - func setPermission(_ allow: Bool, forDomain domain: String, permissionType: PermissionType) { - assert(permissionType.canPersistGrantedDecision || !allow) + func hasPermissionPersisted(forDomain domain: String, permissionType: PermissionType) -> Bool { + return permissions[domain.dropWWW()]?[permissionType] != nil + } + + func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) { + assert(permissionType.canPersistGrantedDecision || decision != .allow) + assert(permissionType.canPersistDeniedDecision || decision != .deny) let storedPermission: StoredPermission let domain = domain.dropWWW() defer { - self.permissionSubject.send( (domain, permissionType, allow) ) + self.permissionSubject.send( (domain, permissionType, decision) ) } if var oldValue = permissions[domain]?[permissionType] { - oldValue.allow = allow + oldValue.decision = decision storedPermission = oldValue - store.update(objectWithId: oldValue.id, allow: allow) + store.update(objectWithId: oldValue.id, decision: decision) } else { do { - storedPermission = try store.add(domain: domain, permissionType: permissionType, allow: allow) + storedPermission = try store.add(domain: domain, permissionType: permissionType, decision: decision) } catch { os_log("PermissionStore: Failed to store permission", type: .error) return @@ -88,18 +92,6 @@ final class PermissionManager: PermissionManagerProtocol { self.permissions[domain, default: [:]][permissionType] = storedPermission } - func removePermission(forDomain domain: String, permissionType: PermissionType) { - let domain = domain.dropWWW() - guard let oldValue = permissions[domain]?[permissionType] else { - assertionFailure("PermissionStore: Failed to remove permission") - return - } - permissions[domain]?[permissionType] = nil - store.remove(objectWithId: oldValue.id) - - self.permissionSubject.send( (domain, permissionType, nil) ) - } - func burnPermissions(except fireproofDomains: FireproofDomains, completion: @escaping () -> Void) { dispatchPrecondition(condition: .onQueue(.main)) diff --git a/DuckDuckGo/Permissions/Model/PermissionModel.swift b/DuckDuckGo/Permissions/Model/PermissionModel.swift index 1cf888c945..450f0309ae 100644 --- a/DuckDuckGo/Permissions/Model/PermissionModel.swift +++ b/DuckDuckGo/Permissions/Model/PermissionModel.swift @@ -77,7 +77,7 @@ final class PermissionModel { self?.permissionManager(permissionManager, didChangePermanentDecisionFor: value.permissionType, forDomain: value.domain, - to: value.grant) + to: value.decision) }.store(in: &cancellables) } @@ -173,17 +173,21 @@ final class PermissionModel { private func permissionManager(_: PermissionManagerProtocol, didChangePermanentDecisionFor permissionType: PermissionType, forDomain domain: String, - to decision: Bool?) { - - // If Always Deny for the current host: revoke the permission - guard webView?.url?.host?.dropWWW() == domain, - decision == false, - self.permissions[permissionType] != nil - else { - return + to decision: PersistedPermissionDecision) { + + // If Always Allow/Deny for the current host: Grant/Revoke the permission + guard webView?.url?.host?.dropWWW() == domain else { return } + + switch (decision, self.permissions[permissionType]) { + case (.deny, .some): + self.revoke(permissionType) + fallthrough + case (.allow, .requested): + while let query = self.authorizationQueries.first(where: { $0.permissions == [permissionType] }) { + query.handleDecision(grant: decision == .allow) + } + default: break } - - self.revoke(permissionType) } // MARK: Pausing/Revoking @@ -206,8 +210,8 @@ final class PermissionModel { func revoke(_ permission: PermissionType) { if let domain = webView?.url?.host, - permissionManager.permission(forDomain: domain, permissionType: permission) == true { - permissionManager.removePermission(forDomain: domain, permissionType: permission) + case .allow = permissionManager.permission(forDomain: domain, permissionType: permission) { + permissionManager.setPermission(.ask, forDomain: domain, permissionType: permission) } self.permissions[permission].revoke() // await deactivation webView?.revokePermissions([permission]) @@ -243,28 +247,34 @@ final class PermissionModel { private func shouldGrantPermission(for permissions: [PermissionType], requestedForDomain domain: String) -> Bool? { for permission in permissions { - var grant: Bool? - if let stored = permissionManager.permission(forDomain: domain, permissionType: permission), - permission.canPersistGrantedDecision || stored != true { - grant = stored + var grant: PersistedPermissionDecision + let stored = permissionManager.permission(forDomain: domain, permissionType: permission) + if case .allow = stored, permission.canPersistGrantedDecision { + grant = .allow + } else if case .deny = stored, permission.canPersistDeniedDecision { + grant = .deny } else if let state = self.permissions[permission] { switch state { // deny if already denied during current page being displayed case .denied, .revoking: - grant = false + grant = .deny // ask otherwise case .disabled, .requested, .active, .inactive, .paused, .reloading: - break + grant = .ask } + } else { + grant = .ask } - if let grant = grant { - if grant == false { - // deny if at least one permission denied permanently - // or during current page being displayed - return false - } // else if grant == true: allow if all permissions allowed permanently - } else { + switch grant { + case .deny: + // deny if at least one permission denied permanently + // or during current page being displayed + return false + case .allow: + // allow if all permissions allowed permanently + break + case .ask: // if at least one permission is not set: ask return nil } diff --git a/DuckDuckGo/Permissions/Model/PermissionStore.swift b/DuckDuckGo/Permissions/Model/PermissionStore.swift index 82e76b1961..f4426cbcfa 100644 --- a/DuckDuckGo/Permissions/Model/PermissionStore.swift +++ b/DuckDuckGo/Permissions/Model/PermissionStore.swift @@ -20,15 +20,16 @@ import Foundation protocol PermissionStore: AnyObject { func loadPermissions() throws -> [PermissionEntity] - func update(objectWithId id: NSManagedObjectID, allow: Bool?, completionHandler: ((Error?) -> Void)?) + func update(objectWithId id: NSManagedObjectID, decision: PersistedPermissionDecision?, completionHandler: ((Error?) -> Void)?) func remove(objectWithId id: NSManagedObjectID, completionHandler: ((Error?) -> Void)?) - func add(domain: String, permissionType: PermissionType, allow: Bool) throws -> StoredPermission + func add(domain: String, permissionType: PermissionType, decision: PersistedPermissionDecision) throws -> StoredPermission func clear(except: [StoredPermission], completionHandler: ((Error?) -> Void)?) } + extension PermissionStore { - func update(objectWithId id: NSManagedObjectID, allow: Bool?) { - update(objectWithId: id, allow: allow, completionHandler: nil) + func update(objectWithId id: NSManagedObjectID, decision: PersistedPermissionDecision?) { + update(objectWithId: id, decision: decision, completionHandler: nil) } func remove(objectWithId id: NSManagedObjectID) { remove(objectWithId: id, completionHandler: nil) @@ -85,7 +86,7 @@ final class LocalPermissionStore: PermissionStore { return entities } - func update(objectWithId id: NSManagedObjectID, allow: Bool?, completionHandler: ((Error?) -> Void)?) { + func update(objectWithId id: NSManagedObjectID, decision: PersistedPermissionDecision?, completionHandler: ((Error?) -> Void)?) { guard let context = context else { return } func mainQueueCompletion(error: Error?) { guard completionHandler != nil else { return } @@ -102,8 +103,8 @@ final class LocalPermissionStore: PermissionStore { return } - if let allow = allow { - managedObject.allow = allow + if let decision = decision { + managedObject.decision = decision } else { context.delete(managedObject) } @@ -119,7 +120,7 @@ final class LocalPermissionStore: PermissionStore { } func remove(objectWithId id: NSManagedObjectID, completionHandler: ((Error?) -> Void)?) { - update(objectWithId: id, allow: nil, completionHandler: completionHandler) + update(objectWithId: id, decision: nil, completionHandler: completionHandler) } func clear(except exceptions: [StoredPermission], completionHandler: ((Error?) -> Void)?) { @@ -152,7 +153,7 @@ final class LocalPermissionStore: PermissionStore { private func performAdd(domain: String, permissionType: PermissionType, - allow: Bool) -> Result? { + decision: PersistedPermissionDecision) -> Result? { guard let context = context else { return nil } var result: Result? @@ -164,7 +165,7 @@ final class LocalPermissionStore: PermissionStore { managedObject.domainEncrypted = domain as NSString managedObject.permissionType = permissionType.rawValue - managedObject.allow = allow + managedObject.decision = decision do { try context.save() @@ -176,11 +177,11 @@ final class LocalPermissionStore: PermissionStore { return result } - func add(domain: String, permissionType: PermissionType, allow: Bool) throws -> StoredPermission { - let result = performAdd(domain: domain, permissionType: permissionType, allow: allow) + func add(domain: String, permissionType: PermissionType, decision: PersistedPermissionDecision) throws -> StoredPermission { + let result = performAdd(domain: domain, permissionType: permissionType, decision: decision) switch result { case .success(let id): - return StoredPermission(id: id, allow: allow) + return StoredPermission(id: id, decision: decision) case .failure(let error): throw error case .none: diff --git a/DuckDuckGo/Permissions/Model/PermissionType.swift b/DuckDuckGo/Permissions/Model/PermissionType.swift index 45b75074e0..a1daaf633d 100644 --- a/DuckDuckGo/Permissions/Model/PermissionType.swift +++ b/DuckDuckGo/Permissions/Model/PermissionType.swift @@ -37,6 +37,14 @@ extension PermissionType { return true } } + var canPersistDeniedDecision: Bool { + switch self { + case .camera, .microphone, .geolocation: + return true + case .popups: + return false + } + } } extension Array where Element == PermissionType { diff --git a/DuckDuckGo/Permissions/Model/Permissions.xcdatamodeld/Permissions.xcdatamodel/contents b/DuckDuckGo/Permissions/Model/Permissions.xcdatamodeld/Permissions.xcdatamodel/contents index c71e4d46d0..153e07215f 100644 --- a/DuckDuckGo/Permissions/Model/Permissions.xcdatamodeld/Permissions.xcdatamodel/contents +++ b/DuckDuckGo/Permissions/Model/Permissions.xcdatamodeld/Permissions.xcdatamodel/contents @@ -1,11 +1,12 @@ - + + - + \ No newline at end of file diff --git a/DuckDuckGo/Permissions/Model/StoredPermission.swift b/DuckDuckGo/Permissions/Model/StoredPermission.swift index ba7cd3dc2d..b2a6b9b6ae 100644 --- a/DuckDuckGo/Permissions/Model/StoredPermission.swift +++ b/DuckDuckGo/Permissions/Model/StoredPermission.swift @@ -18,9 +18,35 @@ import Foundation +enum PersistedPermissionDecision { + case deny + case allow + case ask + + init(allow: Bool, isRemoved: Bool) { + switch (allow, isRemoved) { + case (_, true): + self = .ask + case (true, _): + self = .allow + case (false, _): + self = .deny + } + } + + var boolValue: Bool { + switch self { + case .deny, .ask: + return false + case .allow: + return true + } + } +} + struct StoredPermission: Equatable { let id: NSManagedObjectID - var allow: Bool + var decision: PersistedPermissionDecision } struct PermissionEntity: Equatable { @@ -42,9 +68,28 @@ struct PermissionEntity: Equatable { return nil } - self.permission = StoredPermission(id: managedObject.objectID, allow: managedObject.allow) + self.permission = StoredPermission(id: managedObject.objectID, decision: managedObject.decision) self.domain = domain self.type = permissionType } } + +extension PermissionManagedObject { + + var decision: PersistedPermissionDecision { + get { + return .init(allow: self.allow, isRemoved: self.isRemoved) + } + set { + if case .ask = newValue { + self.isRemoved = true + self.allow = false + } else { + self.allow = newValue.boolValue + self.isRemoved = false + } + } + } + +} diff --git a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift index 9cebfd7557..634c41e28f 100644 --- a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift +++ b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift @@ -93,7 +93,7 @@ final class PermissionContextMenu: NSMenu { private func setupOtherPermissionMenuItems(for permissions: Permissions) { let permanentlyDeniedPermission = permissions.first(where: { - $0.value == .denied && PermissionManager.shared.permission(forDomain: domain, permissionType: $0.key) == false + $0.value == .denied && PermissionManager.shared.permission(forDomain: domain, permissionType: $0.key) == .deny }) // don't display Reload item for permanently denied Permissions var shouldAddReload = permanentlyDeniedPermission == nil @@ -143,7 +143,7 @@ final class PermissionContextMenu: NSMenu { addItem(.separator()) - if PermissionManager.shared.permission(forDomain: domain, permissionType: .popups) == nil { + if case .ask = PermissionManager.shared.permission(forDomain: domain, permissionType: .popups) { addItem(.alwaysAllow(.popups, on: domain, target: self)) } else { addItem(.alwaysAsk(.popups, on: domain, target: self)) @@ -167,15 +167,16 @@ final class PermissionContextMenu: NSMenu { for (permission, state) in permissions { switch state { case .active, .inactive, .paused: - guard permission.canPersistGrantedDecision else { continue } - if PermissionManager.shared.permission(forDomain: domain, permissionType: permission) == nil { + if case .ask = PermissionManager.shared.permission(forDomain: domain, permissionType: permission) { + guard permission.canPersistGrantedDecision else { continue } addItem(.alwaysAllow(permission, on: domain, target: self)) } else { addItem(.alwaysAsk(permission, on: domain, target: self)) } case .denied: - if PermissionManager.shared.permission(forDomain: domain, permissionType: permission) == nil { + if case .ask = PermissionManager.shared.permission(forDomain: domain, permissionType: permission) { + guard permission.canPersistDeniedDecision else { continue } addItem(.alwaysDeny(permission, on: domain, target: self)) } else { addItem(.alwaysAsk(permission, on: domain, target: self)) @@ -310,7 +311,13 @@ private extension NSMenuItem { } static func alwaysAllow(_ permission: PermissionType, on domain: String, target: PermissionContextMenu) -> NSMenuItem { - let title = String(format: UserText.permissionAlwaysAllowDeviceFormat, permission.localizedDescription, domain) + let title: String + if case .popups = permission { + title = String(format: UserText.permissionAlwaysAllowPopupsFormat, domain) + } else { + title = String(format: UserText.permissionAlwaysAllowDeviceFormat, permission.localizedDescription, domain) + } + let item = NSMenuItem(title: title, action: #selector(PermissionContextMenu.alwaysAllowPermission), keyEquivalent: "") diff --git a/DuckDuckGo/Privacy Dashboard/Model/PermissionAuthorizationState.swift b/DuckDuckGo/Privacy Dashboard/Model/PermissionAuthorizationState.swift index e3a11e76c4..4ea88a462b 100644 --- a/DuckDuckGo/Privacy Dashboard/Model/PermissionAuthorizationState.swift +++ b/DuckDuckGo/Privacy Dashboard/Model/PermissionAuthorizationState.swift @@ -22,4 +22,24 @@ enum PermissionAuthorizationState: String, CaseIterable { case ask case grant case deny + + init(decision: PersistedPermissionDecision) { + switch decision { + case .ask: + self = .ask + case .allow: + self = .grant + case .deny: + self = .deny + } + } + + var persistedPermissionDecision: PersistedPermissionDecision { + switch self { + case .ask: return .ask + case .grant: return .allow + case .deny: return .deny + } + } + } diff --git a/DuckDuckGo/Privacy Dashboard/Model/PrivacyDashboardUserScript.swift b/DuckDuckGo/Privacy Dashboard/Model/PrivacyDashboardUserScript.swift index 1b549fbb27..a4cec4ed14 100644 --- a/DuckDuckGo/Privacy Dashboard/Model/PrivacyDashboardUserScript.swift +++ b/DuckDuckGo/Privacy Dashboard/Model/PrivacyDashboardUserScript.swift @@ -137,12 +137,19 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { "permission": item.state.rawValue, "used": usedPermissions[item.permission] != nil, "paused": usedPermissions[item.permission] == .paused, - "options": PermissionAuthorizationState.allCases.compactMap { decision in + "options": PermissionAuthorizationState.allCases.compactMap { decision -> [String: String]? in // don't show Permanently Allow if can't persist Granted Decision - return decision != .grant || item.permission.canPersistGrantedDecision ? [ + switch decision { + case .grant: + guard item.permission.canPersistGrantedDecision else { return nil } + case .deny: + guard item.permission.canPersistDeniedDecision else { return nil } + case .ask: break + } + return [ "id": decision.rawValue, "title": String(format: decision.localizedFormat(for: item.permission), domain) - ] : nil + ] } ] } diff --git a/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift index 598df7b786..8da685efa6 100644 --- a/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift @@ -107,13 +107,13 @@ final class PrivacyDashboardViewController: NSViewController { } let authState: PrivacyDashboardUserScript.AuthorizationState = PermissionType.allCases.compactMap { permissionType in - guard let alwaysGrant = PermissionManager.shared.permission(forDomain: domain, permissionType: permissionType) else { - if usedPermissions[permissionType] != nil { - return (permissionType, .ask) - } + guard PermissionManager.shared.hasPermissionPersisted(forDomain: domain, permissionType: permissionType) + || usedPermissions[permissionType] != nil + else { return nil } - return (permissionType, alwaysGrant ? .grant : .deny) + let decision = PermissionManager.shared.permission(forDomain: domain, permissionType: permissionType) + return (permissionType, .init(decision: decision)) } privacyDashboardScript.setPermissions(usedPermissions, authorizationState: authState, domain: domain, in: webView) @@ -211,12 +211,8 @@ extension PrivacyDashboardViewController: PrivacyDashboardUserScriptDelegate { assertionFailure("PrivacyDashboardViewController: no domain available") return } - switch state { - case .ask: - PermissionManager.shared.removePermission(forDomain: domain, permissionType: permission) - case .deny, .grant: - PermissionManager.shared.setPermission(state == .grant, forDomain: domain, permissionType: permission) - } + + PermissionManager.shared.setPermission(state.persistedPermissionDecision, forDomain: domain, permissionType: permission) } func userScript(_ userScript: PrivacyDashboardUserScript, setPermission permission: PermissionType, paused: Bool) { diff --git a/DuckDuckGo/Windows/View/WindowsManager.swift b/DuckDuckGo/Windows/View/WindowsManager.swift index 7474c2ab9b..48ffbaecab 100644 --- a/DuckDuckGo/Windows/View/WindowsManager.swift +++ b/DuckDuckGo/Windows/View/WindowsManager.swift @@ -36,6 +36,7 @@ final class WindowsManager { @discardableResult class func openNewWindow(with tabCollectionViewModel: TabCollectionViewModel? = nil, droppingPoint: NSPoint? = nil, + contentSize: NSSize? = nil, showWindow: Bool = true, popUp: Bool = false) -> NSWindow? { let mainWindowController = makeNewWindow(tabCollectionViewModel: tabCollectionViewModel, popUp: popUp) @@ -52,10 +53,13 @@ final class WindowsManager { return mainWindowController.window } - class func openNewWindow(with tab: Tab, droppingPoint: NSPoint? = nil, popUp: Bool = false) { + class func openNewWindow(with tab: Tab, droppingPoint: NSPoint? = nil, contentSize: NSSize? = nil, popUp: Bool = false) { let tabCollection = TabCollection() tabCollection.append(tab: tab) - openNewWindow(with: TabCollectionViewModel(tabCollection: tabCollection), droppingPoint: droppingPoint, popUp: popUp) + openNewWindow(with: TabCollectionViewModel(tabCollection: tabCollection), + droppingPoint: droppingPoint, + contentSize: contentSize, + popUp: popUp) } class func openNewWindow(with initialUrl: URL) { @@ -71,17 +75,19 @@ final class WindowsManager { newTab.setContent(.url(initialUrl)) } - class func openPopUpWindow(with tab: Tab) { + class func openPopUpWindow(with tab: Tab, contentSize: NSSize?) { if let mainWindowController = WindowControllersManager.shared.lastKeyMainWindowController, mainWindowController.window?.styleMask.contains(.fullScreen) == true, mainWindowController.window?.isPopUpWindow == false { mainWindowController.mainViewController.tabCollectionViewModel.insertChild(tab: tab, selected: true) } else { - self.openNewWindow(with: tab, popUp: true) + self.openNewWindow(with: tab, contentSize: contentSize, popUp: true) } } - private class func makeNewWindow(tabCollectionViewModel: TabCollectionViewModel? = nil, popUp: Bool = false) -> MainWindowController { + private class func makeNewWindow(tabCollectionViewModel: TabCollectionViewModel? = nil, + contentSize: NSSize? = nil, + popUp: Bool = false) -> MainWindowController { let mainViewController: MainViewController do { mainViewController = try NSException.catch { @@ -99,6 +105,11 @@ final class WindowsManager { #endif } + var contentSize = contentSize ?? NSSize(width: 1024, height: 790) + contentSize.width = min(NSScreen.main?.frame.size.width ?? 1024, max(contentSize.width, 300)) + contentSize.height = min(NSScreen.main?.frame.size.height ?? 790, max(contentSize.height, 300)) + mainViewController.view.frame = NSRect(origin: .zero, size: contentSize) + return MainWindowController(mainViewController: mainViewController, popUp: popUp) } diff --git a/Submodules/duckduckgo-privacy-dashboard b/Submodules/duckduckgo-privacy-dashboard index 3545ba20a9..181e0e2fba 160000 --- a/Submodules/duckduckgo-privacy-dashboard +++ b/Submodules/duckduckgo-privacy-dashboard @@ -1 +1 @@ -Subproject commit 3545ba20a97a62ff6556a5c7c38f499ae9f2d7a2 +Subproject commit 181e0e2fbac24524a8e3d48ddaa45e0508697821 diff --git a/Unit Tests/Permissions/PermissionManagerMock.swift b/Unit Tests/Permissions/PermissionManagerMock.swift index f7f94cc00d..ae7be64654 100644 --- a/Unit Tests/Permissions/PermissionManagerMock.swift +++ b/Unit Tests/Permissions/PermissionManagerMock.swift @@ -29,12 +29,13 @@ final class PermissionManagerMock: PermissionManagerProtocol { var savedPermissions = [String: [PermissionType: Bool]]() - func permission(forDomain domain: String, permissionType: PermissionType) -> Bool? { - savedPermissions[domain.dropWWW()]?[permissionType] + func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision { + guard let allow = savedPermissions[domain.dropWWW()]?[permissionType] else { return .ask } + return PersistedPermissionDecision(allow: allow, isRemoved: false) } - func setPermission(_ permission: Bool, forDomain domain: String, permissionType: PermissionType) { - savedPermissions[domain.dropWWW(), default: [:]][permissionType] = permission + func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) { + savedPermissions[domain.dropWWW(), default: [:]][permissionType] = decision == .ask ? nil : decision.boolValue } func removePermission(forDomain domain: String, permissionType: PermissionType) { diff --git a/Unit Tests/Permissions/PermissionManagerTests.swift b/Unit Tests/Permissions/PermissionManagerTests.swift index 622f5ac715..0947320820 100644 --- a/Unit Tests/Permissions/PermissionManagerTests.swift +++ b/Unit Tests/Permissions/PermissionManagerTests.swift @@ -39,19 +39,19 @@ final class PermissionManagerTests: XCTestCase { let result3 = manager.permission(forDomain: "otherdomain.com", permissionType: .microphone) XCTAssertEqual(store.history, [.load]) - XCTAssertEqual(result1, true) - XCTAssertEqual(result2, false) - XCTAssertNil(result3) + XCTAssertEqual(result1, .allow) + XCTAssertEqual(result2, .deny) + XCTAssertEqual(result3, .ask) } func testWhenLoadPermissionsFailsThenPermissionsInitializedEmpty() { struct SomethingReallyBad: Error {} store.error = SomethingReallyBad() - XCTAssertNil(manager.permission(forDomain: PermissionEntity.entity1.domain, - permissionType: PermissionEntity.entity1.type)) + XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type), + .ask) store.error = nil - manager.setPermission(true, + manager.setPermission(.allow, forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) @@ -61,13 +61,13 @@ final class PermissionManagerTests: XCTestCase { XCTAssertEqual(store.history, [.load, .add(domain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type, - allow: true)]) - XCTAssertEqual(result, true) + decision: .allow)]) + XCTAssertEqual(result, .allow) } func testWhenPermissionUpdatedThenItsValueIsUpdated() { store.permissions = [.entity1] - manager.setPermission(!PermissionEntity.entity1.permission.allow, + manager.setPermission(.deny, forDomain: "www." + PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) @@ -76,34 +76,33 @@ final class PermissionManagerTests: XCTestCase { XCTAssertEqual(store.history, [.load, .update(id: PermissionEntity.entity1.permission.id, - allow: !PermissionEntity.entity1.permission.allow)]) - XCTAssertEqual(result, !PermissionEntity.entity1.permission.allow) + decision: .deny)]) + XCTAssertEqual(result, .deny) } func testWhenPermissionRemovedThenItsValueBecomesNil() { store.permissions = [.entity1] - manager.removePermission(forDomain: PermissionEntity.entity1.domain, - permissionType: PermissionEntity.entity1.type) + manager.setPermission(.ask, forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) let result = manager.permission(forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) XCTAssertEqual(store.history, [.load, - .remove(PermissionEntity.entity1.permission.id)]) - XCTAssertNil(result) + .update(id: PermissionEntity.entity1.permission.id, decision: .ask)]) + XCTAssertEqual(result, .ask) } func testWhenNewPermissionIsSetThenItIsSavedAndUpdated() { store.permissions = [] - XCTAssertNil(manager.permission(forDomain: PermissionEntity.entity1.domain, - permissionType: PermissionEntity.entity1.type)) - XCTAssertNil(manager.permission(forDomain: PermissionEntity.entity2.domain, - permissionType: PermissionEntity.entity2.type)) + XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type), + .ask) + XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity2.domain, permissionType: PermissionEntity.entity2.type), + .ask) - manager.setPermission(true, + manager.setPermission(.allow, forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) - manager.setPermission(false, + manager.setPermission(.deny, forDomain: PermissionEntity.entity2.domain, permissionType: PermissionEntity.entity2.type) @@ -115,12 +114,12 @@ final class PermissionManagerTests: XCTestCase { XCTAssertEqual(store.history, [.load, .add(domain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type, - allow: true), + decision: .allow), .add(domain: PermissionEntity.entity2.domain.dropWWW(), permissionType: PermissionEntity.entity2.type, - allow: false)]) - XCTAssertEqual(result1, true) - XCTAssertEqual(result2, false) + decision: .deny)]) + XCTAssertEqual(result1, .allow) + XCTAssertEqual(result2, .deny) } func testWhenPermissionIsAddedThenSubjectIsPublished() { @@ -128,11 +127,11 @@ final class PermissionManagerTests: XCTestCase { let c = manager.permissionPublisher.sink { value in XCTAssertEqual(value.domain, PermissionEntity.entity1.domain) XCTAssertEqual(value.permissionType, PermissionEntity.entity1.type) - XCTAssertEqual(value.grant, true) + XCTAssertEqual(value.decision, .allow) e.fulfill() } - manager.setPermission(true, + manager.setPermission(.allow, forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) withExtendedLifetime(c) { @@ -145,13 +144,13 @@ final class PermissionManagerTests: XCTestCase { let c = manager.permissionPublisher.sink { value in XCTAssertEqual(value.domain, PermissionEntity.entity1.domain) XCTAssertEqual(value.permissionType, PermissionEntity.entity1.type) - XCTAssertEqual(value.grant, false) + XCTAssertEqual(value.decision, .deny) e.fulfill() } struct AddingError: Error {} store.error = AddingError() - manager.setPermission(false, + manager.setPermission(.deny, forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) withExtendedLifetime(c) { @@ -166,11 +165,11 @@ final class PermissionManagerTests: XCTestCase { let c = manager.permissionPublisher.sink { value in XCTAssertEqual(value.domain, PermissionEntity.entity1.domain) XCTAssertEqual(value.permissionType, PermissionEntity.entity1.type) - XCTAssertEqual(value.grant, !PermissionEntity.entity1.permission.allow) + XCTAssertEqual(value.decision, .deny) e.fulfill() } - manager.setPermission(!PermissionEntity.entity1.permission.allow, + manager.setPermission(.deny, forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) withExtendedLifetime(c) { @@ -185,12 +184,13 @@ final class PermissionManagerTests: XCTestCase { let c = manager.permissionPublisher.sink { value in XCTAssertEqual(value.domain, PermissionEntity.entity2.domain.dropWWW()) XCTAssertEqual(value.permissionType, PermissionEntity.entity2.type) - XCTAssertNil(value.grant) + XCTAssertEqual(value.decision, .ask) e.fulfill() } - manager.removePermission(forDomain: PermissionEntity.entity2.domain, - permissionType: PermissionEntity.entity2.type) + manager.setPermission(.ask, + forDomain: PermissionEntity.entity2.domain, + permissionType: PermissionEntity.entity2.type) withExtendedLifetime(c) { waitForExpectations(timeout: 1) } @@ -207,18 +207,18 @@ final class PermissionManagerTests: XCTestCase { XCTAssertEqual(store.history, [.load, .clear(exceptions: [PermissionEntity.entity1.permission])]) XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type), - true) - XCTAssertNil(manager.permission(forDomain: PermissionEntity.entity2.domain, - permissionType: PermissionEntity.entity2.type)) + .allow) + XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity2.domain, permissionType: PermissionEntity.entity2.type), + .ask) } } fileprivate extension PermissionEntity { - static let entity1 = PermissionEntity(permission: .init(id: .init(), allow: true), + static let entity1 = PermissionEntity(permission: .init(id: .init(), decision: .allow), domain: "duckduckgo.com", type: .camera) - static let entity2 = PermissionEntity(permission: .init(id: .init(), allow: false), + static let entity2 = PermissionEntity(permission: .init(id: .init(), decision: .deny), domain: "www.domain2.com", type: .microphone) } diff --git a/Unit Tests/Permissions/PermissionModelTests.swift b/Unit Tests/Permissions/PermissionModelTests.swift index 98bd1f2eb6..895d2b35a6 100644 --- a/Unit Tests/Permissions/PermissionModelTests.swift +++ b/Unit Tests/Permissions/PermissionModelTests.swift @@ -501,8 +501,8 @@ final class PermissionModelTests: XCTestCase { } func testWhenDeniedPermissionIsStoredThenQueryIsDenied() { - permissionManagerMock.setPermission(true, forDomain: URL.duckDuckGo.host!, permissionType: .camera) - permissionManagerMock.setPermission(false, forDomain: URL.duckDuckGo.host!, permissionType: .microphone) + permissionManagerMock.setPermission(.allow, forDomain: URL.duckDuckGo.host!, permissionType: .camera) + permissionManagerMock.setPermission(.deny, forDomain: URL.duckDuckGo.host!, permissionType: .microphone) let c = model.$authorizationQuery.sink { query in guard query != nil else { return } @@ -521,8 +521,8 @@ final class PermissionModelTests: XCTestCase { } func testWhenGrantedPermissionIsStoredThenQueryIsGranted() { - permissionManagerMock.setPermission(true, forDomain: URL.duckDuckGo.host!, permissionType: .camera) - permissionManagerMock.setPermission(true, forDomain: URL.duckDuckGo.host!, permissionType: .microphone) + permissionManagerMock.setPermission(.allow, forDomain: URL.duckDuckGo.host!, permissionType: .camera) + permissionManagerMock.setPermission(.allow, forDomain: URL.duckDuckGo.host!, permissionType: .microphone) let c = model.$authorizationQuery.sink { query in guard query != nil else { return } @@ -541,7 +541,7 @@ final class PermissionModelTests: XCTestCase { } func testWhenPartialGrantedPermissionIsStoredThenQueryIsQueried() { - permissionManagerMock.setPermission(true, forDomain: URL.duckDuckGo.host!, permissionType: .camera) + permissionManagerMock.setPermission(.allow, forDomain: URL.duckDuckGo.host!, permissionType: .camera) let e = expectation(description: "Permission asked") let c = model.$authorizationQuery.sink { query in @@ -566,7 +566,7 @@ final class PermissionModelTests: XCTestCase { } else { webView.mediaCaptureState = [.activeCamera, .activeMicrophone] } - permissionManagerMock.setPermission(true, forDomain: URL.duckDuckGo.host!, permissionType: .camera) + permissionManagerMock.setPermission(.allow, forDomain: URL.duckDuckGo.host!, permissionType: .camera) let e = expectation(description: "camera stopped") if #available(macOS 12, *) { @@ -583,8 +583,8 @@ final class PermissionModelTests: XCTestCase { } } - permissionManagerMock.setPermission(false, forDomain: URL.duckDuckGo.host!, permissionType: .camera) - permissionManagerMock.permissionSubject.send( (URL.duckDuckGo.host!, .camera, false) ) + permissionManagerMock.setPermission(.deny, forDomain: URL.duckDuckGo.host!, permissionType: .camera) + permissionManagerMock.permissionSubject.send( (URL.duckDuckGo.host!, .camera, .deny) ) waitForExpectations(timeout: 1) } @@ -597,7 +597,7 @@ final class PermissionModelTests: XCTestCase { } else { self.webView.mediaCaptureState = [.activeCamera, .activeMicrophone] } - permissionManagerMock.setPermission(true, forDomain: URL.duckDuckGo.host!, permissionType: .camera) + permissionManagerMock.setPermission(.allow, forDomain: URL.duckDuckGo.host!, permissionType: .camera) if #available(macOS 12, *) { webView.setMicCaptureStateHandler = { _ in @@ -613,7 +613,7 @@ final class PermissionModelTests: XCTestCase { } permissionManagerMock.removePermission(forDomain: URL.duckDuckGo.host!, permissionType: .camera) - permissionManagerMock.permissionSubject.send( (URL.duckDuckGo.host!, .camera, nil) ) + permissionManagerMock.permissionSubject.send( (URL.duckDuckGo.host!, .camera, .ask) ) } func testWhenMicrophoneIsMutedThenSetMediaCaptureMutedIsCalled() { diff --git a/Unit Tests/Permissions/PermissionStoreMock.swift b/Unit Tests/Permissions/PermissionStoreMock.swift index 6bf7b157ae..53dec7965d 100644 --- a/Unit Tests/Permissions/PermissionStoreMock.swift +++ b/Unit Tests/Permissions/PermissionStoreMock.swift @@ -25,9 +25,9 @@ final class PermissionStoreMock: PermissionStore { enum CallHistoryItem: Equatable { case load - case update(id: NSManagedObjectID, allow: Bool?) + case update(id: NSManagedObjectID, decision: PersistedPermissionDecision?) case remove(NSManagedObjectID) - case add(domain: String, permissionType: PermissionType, allow: Bool) + case add(domain: String, permissionType: PermissionType, decision: PersistedPermissionDecision) case clear(exceptions: [StoredPermission]) } @@ -41,8 +41,8 @@ final class PermissionStoreMock: PermissionStore { return permissions } - func update(objectWithId id: NSManagedObjectID, allow: Bool?, completionHandler: ((Error?) -> Void)?) { - history.append(.update(id: id, allow: allow)) + func update(objectWithId id: NSManagedObjectID, decision: PersistedPermissionDecision?, completionHandler: ((Error?) -> Void)?) { + history.append(.update(id: id, decision: decision)) completionHandler?(nil) } @@ -51,12 +51,12 @@ final class PermissionStoreMock: PermissionStore { completionHandler?(nil) } - func add(domain: String, permissionType: PermissionType, allow: Bool) throws -> StoredPermission { - history.append(.add(domain: domain, permissionType: permissionType, allow: allow)) + func add(domain: String, permissionType: PermissionType, decision: PersistedPermissionDecision) throws -> StoredPermission { + history.append(.add(domain: domain, permissionType: permissionType, decision: decision)) if let error = error { throw error } - return StoredPermission(id: .init(), allow: allow) + return StoredPermission(id: .init(), decision: decision) } func clear(except: [StoredPermission], completionHandler: ((Error?) -> Void)?) { diff --git a/Unit Tests/Permissions/PermissionStoreTests.swift b/Unit Tests/Permissions/PermissionStoreTests.swift index dd5e62a55d..9f304178e8 100644 --- a/Unit Tests/Permissions/PermissionStoreTests.swift +++ b/Unit Tests/Permissions/PermissionStoreTests.swift @@ -26,23 +26,23 @@ final class PermissionStoreTests: XCTestCase { lazy var store = LocalPermissionStore(context: container.viewContext) func testWhenPermissionIsAddedThenItMustBeLoadedFromStore() throws { - let stored = try store.add(domain: "duckduckgo.com", permissionType: .camera, allow: true) - XCTAssertTrue(stored.allow) + let stored = try store.add(domain: "duckduckgo.com", permissionType: .camera, decision: .allow) + XCTAssertEqual(stored.decision, .allow) let permissions = try store.loadPermissions() - XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored.id, allow: true), + XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored.id, decision: .allow), domain: "duckduckgo.com", type: .camera)]) } func testWhenPermissionIsRemovedThenItShouldntBeLoadedFromStore() throws { - let stored1 = try store.add(domain: "duckduckgo.com", permissionType: .microphone, allow: true) - let stored2 = try store.add(domain: "otherdomain.com", permissionType: .geolocation, allow: false) + let stored1 = try store.add(domain: "duckduckgo.com", permissionType: .microphone, decision: .allow) + let stored2 = try store.add(domain: "otherdomain.com", permissionType: .geolocation, decision: .deny) let e = expectation(description: "object removed") store.remove(objectWithId: stored2.id) { [store] _ in let permissions = try? store.loadPermissions() - XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, allow: true), + XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, decision: .allow), domain: "duckduckgo.com", type: .microphone)]) e.fulfill() @@ -51,16 +51,16 @@ final class PermissionStoreTests: XCTestCase { } func testWhenPermissionIsUpdatedThenIstLoadedWithNewValue() throws { - let stored1 = try store.add(domain: "duckduckgo.com", permissionType: .microphone, allow: true) - let stored2 = try store.add(domain: "otherdomain.com", permissionType: .geolocation, allow: true) + let stored1 = try store.add(domain: "duckduckgo.com", permissionType: .microphone, decision: .allow) + let stored2 = try store.add(domain: "otherdomain.com", permissionType: .geolocation, decision: .allow) let e = expectation(description: "object removed") - store.update(objectWithId: stored2.id, allow: false) { [store] _ in + store.update(objectWithId: stored2.id, decision: .deny) { [store] _ in let permissions = try? store.loadPermissions() - XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, allow: true), + XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, decision: .allow), domain: "duckduckgo.com", type: .microphone), - .init(permission: StoredPermission(id: stored2.id, allow: false), + .init(permission: StoredPermission(id: stored2.id, decision: .deny), domain: "otherdomain.com", type: .geolocation)]) e.fulfill() @@ -69,10 +69,10 @@ final class PermissionStoreTests: XCTestCase { } func testWhenPermissionsAreClearedThenOnlyExceptionsRemain() throws { - let stored1 = try store.add(domain: "duckduckgo.com", permissionType: .microphone, allow: true) - _=try store.add(domain: "otherdomain.com", permissionType: .geolocation, allow: true) - let stored3 = try store.add(domain: "wikipedia.org", permissionType: .camera, allow: false) - _=try store.add(domain: "permission.site", permissionType: .microphone, allow: false) + let stored1 = try store.add(domain: "duckduckgo.com", permissionType: .microphone, decision: .allow) + _=try store.add(domain: "otherdomain.com", permissionType: .geolocation, decision: .allow) + let stored3 = try store.add(domain: "wikipedia.org", permissionType: .camera, decision: .deny) + _=try store.add(domain: "permission.site", permissionType: .microphone, decision: .deny) let e = expectation(description: "store cleared") store.clear(except: [stored1, stored3]) { [store] error in @@ -80,10 +80,10 @@ final class PermissionStoreTests: XCTestCase { let permissions = try! store.loadPermissions() // swiftlint:disable:this force_try - XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, allow: true), + XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, decision: .allow), domain: "duckduckgo.com", type: .microphone), - .init(permission: StoredPermission(id: stored3.id, allow: false), + .init(permission: StoredPermission(id: stored3.id, decision: .deny), domain: "wikipedia.org", type: .camera)]) From 4570364717ad7c8def14f1f56029ea98f9860e87 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 12 Jan 2022 19:37:23 +0700 Subject: [PATCH 2/9] Update lottie dependency (#395) --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 15 +++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ddf1b46365..69712a8dde 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5143,8 +5143,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/airbnb/lottie-ios"; requirement = { - branch = "lottie/macos-spm"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 3.3.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f4bd2fbc17..840e4c1e95 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,9 +23,9 @@ "package": "Lottie", "repositoryURL": "https://github.com/airbnb/lottie-ios", "state": { - "branch": "lottie/macos-spm", - "revision": "f4d701070f9743b9ef7b9ac42d386d1ef98b842f", - "version": null + "branch": null, + "revision": "4a6058cbbdfe4f74aeae92c8bd51ad3b0de2a1ee", + "version": "3.3.0" } }, { @@ -63,15 +63,6 @@ "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", "version": "0.5.0" } - }, - { - "package": "TrackerRadarKit", - "repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit.git", - "state": { - "branch": null, - "revision": "5f4caf35b8418700a48c64c7c61eb43308c8dacc", - "version": "1.0.3" - } } ] }, From 88c49d414160163bae49454f210a317aabf838cd Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 12 Jan 2022 20:11:44 +0700 Subject: [PATCH 3/9] Fire Performance improvement; Async/await APIs (#380) * Fire Performance improvement; Async/await APIs * fix cache files paths * Recreated Caches WebKit directory after removal * rm random files * Use ephemeral URLSession for API requests * Fix Common/Extensions ordering * fix test * fix dependency: set lottie version --- DuckDuckGo.xcodeproj/project.pbxproj | 54 ++++--- .../xcshareddata/swiftpm/Package.resolved | 9 ++ DuckDuckGo/API/APIRequest.swift | 18 +-- .../Services/WebsiteDataStore.swift | 146 ++++++++---------- .../Common/Extensions/ProcessExtension.swift | 32 ++++ .../Extensions/URLSessionExtension.swift | 34 ++++ .../Model/CrashReportSender.swift | 2 +- .../Email/EmailManagerRequestDelegate.swift | 2 +- DuckDuckGo/Fire/Model/Fire.swift | 32 ++-- .../Model/SuggestionContainer.swift | 2 +- .../Services/WebsiteDataStoreMock.swift | 9 +- .../Services/WebsiteDataStoreTests.swift | 35 +++-- Unit Tests/Fire/Model/FireTests.swift | 6 +- 13 files changed, 208 insertions(+), 173 deletions(-) create mode 100644 DuckDuckGo/Common/Extensions/ProcessExtension.swift create mode 100644 DuckDuckGo/Common/Extensions/URLSessionExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 69712a8dde..00eb74d919 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -444,7 +444,6 @@ AAF7D3862567CED500998667 /* WebViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF7D3852567CED500998667 /* WebViewConfiguration.swift */; }; AAFCB37F25E545D400859DD4 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */; }; AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */; }; - B6106B9E26A565DA0013B453 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; B6106BA026A7BE0B0013B453 /* PermissionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9F26A7BE0B0013B453 /* PermissionManagerTests.swift */; }; B6106BA426A7BEA40013B453 /* PermissionAuthorizationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA226A7BEA00013B453 /* PermissionAuthorizationState.swift */; }; B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA526A7BEC80013B453 /* PermissionAuthorizationQuery.swift */; }; @@ -519,6 +518,7 @@ B688B4DA273E6D3B0087BEAF /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B688B4D9273E6D3B0087BEAF /* MainView.swift */; }; B688B4DF27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B688B4DE27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift */; }; B689ECD526C247DB006FB0C5 /* BackForwardListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B689ECD426C247DB006FB0C5 /* BackForwardListItem.swift */; }; + B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */; }; B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C0274E3EF4002AC6B0 /* PopUpWindow.swift */; }; B68C92C42750EF76002AC6B0 /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; }; B693954A26F04BEB0015B914 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953C26F04BE70015B914 /* NibLoadable.swift */; }; @@ -609,6 +609,8 @@ B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA441D2616C84600DD1EC2 /* PixelStoreMock.swift */; }; B6DA44232616CABC00DD1EC2 /* PixelArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44222616CABC00DD1EC2 /* PixelArgumentsTests.swift */; }; B6DA44282616CAE000DD1EC2 /* AppUsageActivityMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44272616CAE000DD1EC2 /* AppUsageActivityMonitorTests.swift */; }; + B6DB3AEF278D5C370024C5C4 /* URLSessionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */; }; + B6DB3AF6278EA0130024C5C4 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; B6DB3CF926A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DB3CF826A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift */; }; B6DB3CFB26A17CB800D459B7 /* PermissionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DB3CFA26A17CB800D459B7 /* PermissionModel.swift */; }; B6E53883267C83420010FEA9 /* HomepageBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E53882267C83420010FEA9 /* HomepageBackgroundView.swift */; }; @@ -1062,7 +1064,6 @@ AAC9C01B24CB594C00AD1325 /* TabViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewModelTests.swift; sourceTree = ""; }; AAC9C01D24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollectionViewModelTests.swift; sourceTree = ""; }; AACF6FD526BC366D00CF09F9 /* SafariVersionReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariVersionReader.swift; sourceTree = ""; }; - AAD2F8B026BC3F55003C5DC8 /* BundleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtension.swift; sourceTree = ""; }; AAD6D8862696DF6D002393B3 /* CrashReportPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportPromptViewController.swift; sourceTree = ""; }; AAD86E502678D104005C11BE /* DuckDuckGoCI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoCI.entitlements; sourceTree = ""; }; AAD86E51267A0DFF005C11BE /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateController.swift; sourceTree = ""; }; @@ -1172,6 +1173,7 @@ B688B4D9273E6D3B0087BEAF /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; B688B4DE27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSearchTextMenuItemHandler.swift; sourceTree = ""; }; B689ECD426C247DB006FB0C5 /* BackForwardListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackForwardListItem.swift; sourceTree = ""; }; + B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessExtension.swift; sourceTree = ""; }; B68C92C0274E3EF4002AC6B0 /* PopUpWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpWindow.swift; sourceTree = ""; }; B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelDataRecord.swift; sourceTree = ""; }; B693953C26F04BE70015B914 /* NibLoadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; @@ -1263,6 +1265,7 @@ B6DA441D2616C84600DD1EC2 /* PixelStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelStoreMock.swift; sourceTree = ""; }; B6DA44222616CABC00DD1EC2 /* PixelArgumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelArgumentsTests.swift; sourceTree = ""; }; B6DA44272616CAE000DD1EC2 /* AppUsageActivityMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageActivityMonitorTests.swift; sourceTree = ""; }; + B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionExtension.swift; sourceTree = ""; }; B6DB3CF826A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice+SwizzledAuthState.swift"; sourceTree = ""; }; B6DB3CFA26A17CB800D459B7 /* PermissionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionModel.swift; sourceTree = ""; }; B6E53882267C83420010FEA9 /* HomepageBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageBackgroundView.swift; sourceTree = ""; }; @@ -2897,63 +2900,64 @@ AADC60E92493B305008F8EF7 /* Extensions */ = { isa = PBXGroup; children = ( - 4B8D9061276D1D880078DB17 /* LocaleExtension.swift */, - 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */, + B6DB3CF826A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift */, AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */, - AAD2F8B026BC3F55003C5DC8 /* BundleExtension.swift */, + B6106B9D26A565DA0013B453 /* BundleExtension.swift */, 4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */, 85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */, B6A9E46F26146A250067D1B9 /* DateExtension.swift */, B63D467025BFA6C100874977 /* DispatchQueueExtensions.swift */, AA92126E25ACCB1100600CD4 /* ErrorExtension.swift */, + B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */, AAECA41F24EEA4AC00EFA63A /* IndexPathExtension.swift */, + 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */, + 4B8D9061276D1D880078DB17 /* LocaleExtension.swift */, 85308E24267FC9F2001ABD76 /* NSAlertExtension.swift */, F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */, AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */, + B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */, B63D467925BFC3E100874977 /* NSCoderExtensions.swift */, F41D174025CB131900472416 /* NSColorExtension.swift */, + B657841825FA484B00D8DB33 /* NSException+Catch.h */, + B657841925FA484B00D8DB33 /* NSException+Catch.m */, + B657841E25FA497600D8DB33 /* NSException+Catch.swift */, + 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */, AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */, AA72D5FD25FFF94E00C77619 /* NSMenuItemExtension.swift */, 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */, + 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */, AA5C8F5D2590EEE800748EB7 /* NSPointExtension.swift */, + 85625999269CA0A600EE44BC /* NSRectExtension.swift */, B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */, AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */, + 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, AA5C8F58258FE21F00748EB7 /* NSTextFieldExtension.swift */, + 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */, AA6FFB4324DC33320028F4D0 /* NSViewExtension.swift */, AA9E9A5525A3AE8400D1959D /* NSWindowExtension.swift */, + B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */, + B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */, B684592125C93BE000DC17B6 /* Publisher.asVoid.swift */, + B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */, + AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */, B684592625C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift */, B6AAAC3D26048F690029438D /* RandomAccessCollectionExtension.swift */, + B6C0B24326E9CB080031CB7F /* RunLoopExtension.swift */, 4BB88B4925B7B690006F6B06 /* SequenceExtensions.swift */, B65783E625F8AAFB00D8DB33 /* String+Punycode.swift */, AA8EDF2624923EC70071C2E8 /* StringExtension.swift */, + AAADFD05264AA282001555EA /* TimeIntervalExtension.swift */, AA8EDF2324923E980071C2E8 /* URLExtension.swift */, AA88D14A252A557100980B4E /* URLRequestExtension.swift */, + B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, + 336B39E62726BAE800C417D3 /* UserDefaultsExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, B63D466825BEB6C200874977 /* WKWebView+SessionState.swift */, B68458CC25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift */, AA92127625ADA07900600CD4 /* WKWebViewExtension.swift */, B6CF78DD267B099C00CD4F13 /* WKNavigationActionExtension.swift */, - B6DB3CF826A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift */, - AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */, - B657841825FA484B00D8DB33 /* NSException+Catch.h */, - B657841925FA484B00D8DB33 /* NSException+Catch.m */, - B657841E25FA497600D8DB33 /* NSException+Catch.swift */, - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, - B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */, - B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */, - AAADFD05264AA282001555EA /* TimeIntervalExtension.swift */, - B6106B9D26A565DA0013B453 /* BundleExtension.swift */, - 85625999269CA0A600EE44BC /* NSRectExtension.swift */, - 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, - 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */, - B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */, - B65E6B9F26D9F10600095F96 /* NSBezierPathExtension.swift */, - B6C0B24326E9CB080031CB7F /* RunLoopExtension.swift */, - 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */, - 336B39E62726BAE800C417D3 /* UserDefaultsExtension.swift */, 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */, ); path = Extensions; @@ -3696,6 +3700,7 @@ AA80EC54256BE3BC007083E7 /* UserText.swift in Sources */, B61EF3EC266F91E700B4D78F /* WKWebView+Download.swift in Sources */, B637274426CE25EF00C8CB02 /* NSApplication+BuildTime.m in Sources */, + B6DB3AEF278D5C370024C5C4 /* URLSessionExtension.swift in Sources */, 4B7A60A1273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift in Sources */, B693955326F04BEC0015B914 /* WindowDraggingView.swift in Sources */, 4B0511C4262CAA5A00F6079C /* PreferencesSidebarViewController.swift in Sources */, @@ -3864,6 +3869,7 @@ AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, 4B0511C9262CAA5A00F6079C /* RoundedSelectionRowView.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* BookmarkPopover.swift in Sources */, + B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, B6C0B24426E9CB080031CB7F /* RunLoopExtension.swift in Sources */, @@ -3996,7 +4002,7 @@ B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */, 4B92929D26670D2A00AD2C21 /* BookmarkNode.swift in Sources */, B693955226F04BEB0015B914 /* LongPressButton.swift in Sources */, - B6106B9E26A565DA0013B453 /* BundleExtension.swift in Sources */, + B6DB3AF6278EA0130024C5C4 /* BundleExtension.swift in Sources */, 4B677438255DBEB800025BD8 /* HTTPSUpgrade.xcdatamodeld in Sources */, 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */, AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 840e4c1e95..67e2b3cd9a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -63,6 +63,15 @@ "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", "version": "0.5.0" } + }, + { + "package": "TrackerRadarKit", + "repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit.git", + "state": { + "branch": null, + "revision": "5f4caf35b8418700a48c64c7c61eb43308c8dacc", + "version": "1.0.3" + } } ] }, diff --git a/DuckDuckGo/API/APIRequest.swift b/DuckDuckGo/API/APIRequest.swift index 957aac2084..650d61a21c 100644 --- a/DuckDuckGo/API/APIRequest.swift +++ b/DuckDuckGo/API/APIRequest.swift @@ -23,18 +23,7 @@ import os.log typealias APIRequestCompletion = (APIRequest.Response?, Error?) -> Void enum APIRequest { - - private static var defaultCallbackQueue: OperationQueue = { - let queue = OperationQueue() - queue.name = "APIRequest default callback queue" - queue.qualityOfService = .utility - queue.maxConcurrentOperationCount = 1 - return queue - }() - - private static let defaultSession = URLSession(configuration: .default, delegate: nil, delegateQueue: defaultCallbackQueue) - private static let mainThreadCallbackSession = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main) - + struct Response { var data: Data? @@ -66,10 +55,10 @@ enum APIRequest { let urlRequest = urlRequestFor(url: url, method: method, parameters: parameters, headers: headers, timeoutInterval: timeoutInterval) - let session = callBackOnMainThread ? mainThreadCallbackSession : defaultSession + let session: URLSession = callBackOnMainThread ? .mainThreadCallbackSession : .default let task = session.dataTask(with: urlRequest) { (data, response, error) in - + let httpResponse = response as? HTTPURLResponse if let error = error { @@ -100,6 +89,7 @@ enum APIRequest { urlRequest.timeoutInterval = timeoutInterval return urlRequest } + } extension HTTPURLResponse { diff --git a/DuckDuckGo/BrowserTab/Services/WebsiteDataStore.swift b/DuckDuckGo/BrowserTab/Services/WebsiteDataStore.swift index a76c988a4a..5abe4b0d8f 100644 --- a/DuckDuckGo/BrowserTab/Services/WebsiteDataStore.swift +++ b/DuckDuckGo/BrowserTab/Services/WebsiteDataStore.swift @@ -21,24 +21,16 @@ import GRDB import os public protocol HTTPCookieStore { - - func getAllCookies(_ completionHandler: @escaping ([HTTPCookie]) -> Void) - - func setCookie(_ cookie: HTTPCookie, completionHandler: (() -> Void)?) - - func delete(_ cookie: HTTPCookie, completionHandler: (() -> Void)?) - + func allCookies() async -> [HTTPCookie] + func setCookie(_ cookie: HTTPCookie) async + func deleteCookie(_ cookie: HTTPCookie) async } protocol WebsiteDataStore { - var cookieStore: HTTPCookieStore? { get } - func fetchDataRecords(ofTypes dataTypes: Set, completionHandler: @escaping ([WKWebsiteDataRecord]) -> Void) - - func removeData(ofTypes dataTypes: Set, for dataRecords: [WKWebsiteDataRecord], completionHandler: @escaping () -> Void) - func removeData(ofTypes dataTypes: Set, modifiedSince date: Date, completionHandler: @escaping () -> Void) - + func dataRecords(ofTypes dataTypes: Set) async -> [WKWebsiteDataRecord] + func removeData(ofTypes dataTypes: Set, modifiedSince date: Date) async } internal class WebCacheManager { @@ -54,97 +46,81 @@ internal class WebCacheManager { self.websiteDataStore = websiteDataStore } - func clear(completion: @escaping () -> Void) { + func clear(domains: Set? = nil) async { + // first cleanup ~/Library/Caches + await self.clearFileCache() - let types = WKWebsiteDataStore.allWebsiteDataTypesExceptCookies + await removeAllDataExceptCookies() - websiteDataStore.removeData(ofTypes: types, modifiedSince: Date.distantPast) { - guard let cookieStore = self.websiteDataStore.cookieStore else { - completion() - return - } + await removeCookies(forDomains: domains) - cookieStore.getAllCookies { cookies in - let group = DispatchGroup() - // Don't clear fireproof domains - let cookiesToRemove = cookies.filter { cookie in - !self.fireproofDomains.isFireproof(cookieDomain: cookie.domain) && cookie.domain != URL.cookieDomain - } + await self.removeResourceLoadStatisticsDatabase() + } - for cookie in cookiesToRemove { - group.enter() - os_log("Deleting cookie for %s named %s", log: .fire, cookie.domain, cookie.name) - cookieStore.delete(cookie) { - group.leave() - } - } - - self.removeResourceLoadStatisticsDatabase() + private func clearFileCache() async { + let fm = FileManager.default + let cachesDir = fm.urls(for: .cachesDirectory, in: .userDomainMask).first! + .appendingPathComponent(Bundle.main.bundleIdentifier!) + let tmpDir = fm.temporaryDirectory(appropriateFor: cachesDir).appendingPathComponent(UUID().uuidString) - group.notify(queue: .main) { - completion() - } - } + do { + try fm.createDirectory(at: tmpDir, withIntermediateDirectories: false, attributes: nil) + } catch { + os_log("Could not create temporary directory: %s", type: .error, "\(error)") + return } - } - func clear(domains: Set? = nil, - completion: @escaping () -> Void) { + let contents = try? fm.contentsOfDirectory(atPath: cachesDir.path) + for name in contents ?? [] { + guard ["WebKit", "fsCachedData"].contains(name) || name.hasPrefix("Cache.") else { continue } + try? fm.moveItem(at: cachesDir.appendingPathComponent(name), to: tmpDir.appendingPathComponent(name)) + } - let all = WKWebsiteDataStore.allWebsiteDataTypes() - let allExceptCookies = WKWebsiteDataStore.allWebsiteDataTypesExceptCookies + try? fm.createDirectory(at: cachesDir.appendingPathComponent("WebKit"), + withIntermediateDirectories: false, + attributes: nil) - websiteDataStore.fetchDataRecords(ofTypes: all) { [weak self] records in + Process("/bin/rm", "-rf", tmpDir.path).launch() + } - // Remove all data except cookies for all domains, and then filter cookies to preserve those allowed by Fireproofing. - self?.websiteDataStore.removeData(ofTypes: allExceptCookies, for: records) { [weak self] in - guard let self = self else { return } + @MainActor + private func removeAllDataExceptCookies() async { + let allExceptCookies = WKWebsiteDataStore.allWebsiteDataTypesExceptCookies - guard let cookieStore = self.websiteDataStore.cookieStore else { - completion() - return - } + // Remove all data except cookies for all domains, and then filter cookies to preserve those allowed by Fireproofing. + await websiteDataStore.removeData(ofTypes: allExceptCookies, modifiedSince: Date.distantPast) + } - cookieStore.getAllCookies { cookies in - let group = DispatchGroup() - - var cookies = cookies - if let domains = domains { - // If domains are specified, clear just their cookies - cookies = cookies.filter { cookie in - domains.contains { - $0 == cookie.domain - || ".\($0)" == cookie.domain - || (cookie.domain.hasPrefix(".") && $0.hasSuffix(cookie.domain)) - } - } - } - // Don't clear fireproof domains - let cookiesToRemove = cookies.filter { cookie in - !self.fireproofDomains.isFireproof(cookieDomain: cookie.domain) && cookie.domain != URL.cookieDomain - } - - for cookie in cookiesToRemove { - group.enter() - os_log("Deleting cookie for %s named %s", log: .fire, cookie.domain, cookie.name) - cookieStore.delete(cookie) { - group.leave() - } - } - - self.removeResourceLoadStatisticsDatabase() - - group.notify(queue: .main) { - completion() - } + @MainActor + private func removeCookies(forDomains domains: Set? = nil) async { + guard let cookieStore = websiteDataStore.cookieStore else { return } + var cookies = await cookieStore.allCookies() + + if let domains = domains { + // If domains are specified, clear just their cookies + cookies = cookies.filter { cookie in + domains.contains { + $0 == cookie.domain + || ".\($0)" == cookie.domain + || (cookie.domain.hasPrefix(".") && $0.hasSuffix(cookie.domain)) } } } + + // Don't clear fireproof domains + let cookiesToRemove = cookies.filter { cookie in + !self.fireproofDomains.isFireproof(cookieDomain: cookie.domain) && cookie.domain != URL.cookieDomain + } + + for cookie in cookiesToRemove { + os_log("Deleting cookie for %s named %s", log: .fire, cookie.domain, cookie.name) + await cookieStore.deleteCookie(cookie) + } } // WKWebView doesn't provide a way to remove the observations database, which contains domains that have been // visited by the user. This database is removed directly as a part of the Fire button process. - private func removeResourceLoadStatisticsDatabase() { + private func removeResourceLoadStatisticsDatabase() async { guard let bundleID = Bundle.main.bundleIdentifier, var libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else { return diff --git a/DuckDuckGo/Common/Extensions/ProcessExtension.swift b/DuckDuckGo/Common/Extensions/ProcessExtension.swift new file mode 100644 index 0000000000..5c0ad5efea --- /dev/null +++ b/DuckDuckGo/Common/Extensions/ProcessExtension.swift @@ -0,0 +1,32 @@ +// +// ProcessExtension.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Process { + + convenience init(_ command: String, _ args: String..., workDirectory: URL? = nil) { + self.init() + self.executableURL = URL(fileURLWithPath: command) + self.arguments = args + if let workDirectory = workDirectory { + self.currentDirectoryURL = workDirectory + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/URLSessionExtension.swift b/DuckDuckGo/Common/Extensions/URLSessionExtension.swift new file mode 100644 index 0000000000..e92dd10841 --- /dev/null +++ b/DuckDuckGo/Common/Extensions/URLSessionExtension.swift @@ -0,0 +1,34 @@ +// +// URLSessionExtension.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension URLSession { + + private static var defaultCallbackQueue: OperationQueue = { + let queue = OperationQueue() + queue.name = "APIRequest default callback queue" + queue.qualityOfService = .utility + queue.maxConcurrentOperationCount = 1 + return queue + }() + + static let `default` = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: defaultCallbackQueue) + static let mainThreadCallbackSession = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: OperationQueue.main) + +} diff --git a/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift b/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift index a9972da39f..fa60f57789 100644 --- a/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift +++ b/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift @@ -35,7 +35,7 @@ final class CrashReportSender { request.httpMethod = "POST" request.httpBody = contentData - URLSession.shared.dataTask(with: request) { (_, _, error) in + URLSession.default.dataTask(with: request) { (_, _, error) in if error != nil { assertionFailure("CrashReportSender: Failed to send the crash reprot") } diff --git a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift index acdb7184a7..24b15bb537 100644 --- a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift +++ b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift @@ -43,7 +43,7 @@ extension EmailManagerRequestDelegate { request.allHTTPHeaderFields = headers request.httpMethod = method request.httpBody = httpBody - URLSession.shared.dataTask(with: request) { (data, _, error) in + URLSession.default.dataTask(with: request) { (data, _, error) in currentQueue?.addOperation { completion(data, error) } diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index b83a5449f8..e5bcaedca2 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -57,9 +57,10 @@ final class Fire { let burningDomains = domains.union(wwwDomains) group.enter() - burnWebCache(domains: burningDomains, completion: { + Task { + await burnWebCache(domains: burningDomains) group.leave() - }) + } group.enter() burnHistory(of: burningDomains, completion: { @@ -89,7 +90,8 @@ final class Fire { let group = DispatchGroup() group.enter() - burnWebCache { + Task { + await burnWebCache() group.leave() } @@ -116,28 +118,16 @@ final class Fire { // MARK: - Web cache - private func burnWebCache(completion: @escaping () -> Void) { + private func burnWebCache() async { os_log("WebsiteDataStore began cookie deletion", log: .fire) - - webCacheManager.clear { - os_log("WebsiteDataStore completed cookie deletion", log: .fire) - - DispatchQueue.main.async { - completion() - } - } + await webCacheManager.clear() + os_log("WebsiteDataStore completed cookie deletion", log: .fire) } - private func burnWebCache(domains: Set? = nil, completion: @escaping () -> Void) { + private func burnWebCache(domains: Set? = nil) async { os_log("WebsiteDataStore began cookie deletion", log: .fire) - - webCacheManager.clear(domains: domains) { - os_log("WebsiteDataStore completed cookie deletion", log: .fire) - - DispatchQueue.main.async { - completion() - } - } + await webCacheManager.clear(domains: domains) + os_log("WebsiteDataStore completed cookie deletion", log: .fire) } // MARK: - History diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 1bccd75d06..120a0efccc 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -106,7 +106,7 @@ extension SuggestionContainer: SuggestionLoadingDataSource { var request = URLRequest.defaultRequest(with: url) request.timeoutInterval = 1 - URLSession.shared.dataTask(with: request) { (data, _, error) in + URLSession.default.dataTask(with: request) { (data, _, error) in completion(data, error) }.resume() } diff --git a/Unit Tests/BrowserTab/Services/WebsiteDataStoreMock.swift b/Unit Tests/BrowserTab/Services/WebsiteDataStoreMock.swift index bb2dbe029a..0707dafc40 100644 --- a/Unit Tests/BrowserTab/Services/WebsiteDataStoreMock.swift +++ b/Unit Tests/BrowserTab/Services/WebsiteDataStoreMock.swift @@ -24,15 +24,8 @@ import XCTest final class WebCacheManagerMock: WebCacheManager { var clearCalled = false - override func clear(domains: Set? = nil, - completion: @escaping () -> Void) { + override func clear(domains: Set? = nil) async { clearCalled = true - completion() - } - - override func clear(completion: @escaping () -> Void) { - clearCalled = true - completion() } } diff --git a/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift b/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift index c2ea94de62..8244dc8892 100644 --- a/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift +++ b/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift @@ -44,7 +44,8 @@ final class WebCacheManagerTests: XCTestCase { let expect = expectation(description: #function) let webCacheManager = WebCacheManager(fireproofDomains: logins, websiteDataStore: dataStore) - webCacheManager.clear { + Task { + await webCacheManager.clear() expect.fulfill() } wait(for: [expect], timeout: 15.0) @@ -74,7 +75,8 @@ final class WebCacheManagerTests: XCTestCase { let expect = expectation(description: #function) let webCacheManager = WebCacheManager(fireproofDomains: logins, websiteDataStore: dataStore) - webCacheManager.clear { + Task { + await webCacheManager.clear() expect.fulfill() } wait(for: [expect], timeout: 30.0) @@ -101,7 +103,8 @@ final class WebCacheManagerTests: XCTestCase { let expect = expectation(description: #function) let webCacheManager = WebCacheManager(fireproofDomains: logins, websiteDataStore: dataStore) - webCacheManager.clear { + Task { + await webCacheManager.clear() expect.fulfill() } wait(for: [expect], timeout: 30.0) @@ -129,7 +132,8 @@ final class WebCacheManagerTests: XCTestCase { let expect = expectation(description: #function) let webCacheManager = WebCacheManager(fireproofDomains: logins, websiteDataStore: dataStore) - webCacheManager.clear { + Task { + await webCacheManager.clear() expect.fulfill() } wait(for: [expect], timeout: 30.0) @@ -145,7 +149,8 @@ final class WebCacheManagerTests: XCTestCase { let expect = expectation(description: #function) let webCacheManager = WebCacheManager(fireproofDomains: logins, websiteDataStore: dataStore) - webCacheManager.clear { + Task { + await webCacheManager.clear() expect.fulfill() } wait(for: [expect], timeout: 5.0) @@ -161,22 +166,20 @@ final class WebCacheManagerTests: XCTestCase { var records = [WKWebsiteDataRecord]() var removeDataCalledCount = 0 - func fetchDataRecords(ofTypes dataTypes: Set, completionHandler: @escaping ([WKWebsiteDataRecord]) -> Void) { - completionHandler(records) + func dataRecords(ofTypes dataTypes: Set) async -> [WKWebsiteDataRecord] { + return records } - func removeData(ofTypes dataTypes: Set, for dataRecords: [WKWebsiteDataRecord], completionHandler: @escaping () -> Void) { + func removeData(ofTypes dataTypes: Set, modifiedSince date: Date) async { removeDataCalledCount += 1 // In the real implementation, records will be selectively removed or edited based on their Fireproof status. For simplicity in this test, // only remove records if all data types are removed, so that we can tell whether records for given domains still exist in some form. if dataTypes == WKWebsiteDataStore.allWebsiteDataTypes() { self.records = records.filter { - !dataRecords.contains($0) && dataTypes == $0.dataTypes + dataTypes == $0.dataTypes } } - - completionHandler() } func removeData(ofTypes dataTypes: Set, modifiedSince date: Date, completionHandler: @escaping () -> Void) { @@ -229,18 +232,16 @@ final class WebCacheManagerTests: XCTestCase { self.cookies = cookies } - func getAllCookies(_ completionHandler: @escaping ([HTTPCookie]) -> Void) { - completionHandler(cookies) + func allCookies() async -> [HTTPCookie] { + return cookies } - func setCookie(_ cookie: HTTPCookie, completionHandler: (() -> Void)?) { + func setCookie(_ cookie: HTTPCookie) async { cookies.append(cookie) - completionHandler?() } - func delete(_ cookie: HTTPCookie, completionHandler: (() -> Void)?) { + func deleteCookie(_ cookie: HTTPCookie) async { cookies.removeAll { $0.domain == cookie.domain } - completionHandler?() } } diff --git a/Unit Tests/Fire/Model/FireTests.swift b/Unit Tests/Fire/Model/FireTests.swift index fd00ac38a5..3af4aa98e7 100644 --- a/Unit Tests/Fire/Model/FireTests.swift +++ b/Unit Tests/Fire/Model/FireTests.swift @@ -57,8 +57,12 @@ final class FireTests: XCTestCase { permissionManager: permissionManager) let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel - fire.burnAll(tabCollectionViewModel: tabCollectionViewModel) + let finishedBurningExpectation = expectation(description: "Finished burning") + fire.burnAll(tabCollectionViewModel: tabCollectionViewModel) { + finishedBurningExpectation.fulfill() + } + waitForExpectations(timeout: 1) XCTAssert(manager.clearCalled) XCTAssert(historyCoordinator.burnCalled) XCTAssert(permissionManager.burnPermissionsCalled) From 6047214be9f6e68a5dcb534f6d4d9e895850975e Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Thu, 13 Jan 2022 11:21:14 +0100 Subject: [PATCH 4/9] NSKeyedUnarchiver.unarchivedObject refactored to suppress unncesessary console logs (#396) --- DuckDuckGo/Statistics/PixelDataRecord.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Statistics/PixelDataRecord.swift b/DuckDuckGo/Statistics/PixelDataRecord.swift index b74b5ba354..392a4892e0 100644 --- a/DuckDuckGo/Statistics/PixelDataRecord.swift +++ b/DuckDuckGo/Statistics/PixelDataRecord.swift @@ -37,9 +37,10 @@ extension PixelData { assertionFailure("PixelData: valueEncrypted is not Data") return nil } - if let string = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: data) { - return PixelDataRecord(key: key, value: string as NSString) - } else if let number = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: data) { + let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSString.self, NSNumber.self], from: data) + if let string = unarchived as? NSString { + return PixelDataRecord(key: key, value: string) + } else if let number = unarchived as? NSNumber { return PixelDataRecord(key: key, value: number) } else { return nil From 46b5ff0d4b349b1ad1a9b5248676f7c5b272c17d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 14 Jan 2022 23:45:04 +0700 Subject: [PATCH 5/9] Fix Inspector Hiding crashing in DEBUG (#394) --- DuckDuckGo/BrowserTab/View/WebView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DuckDuckGo/BrowserTab/View/WebView.swift b/DuckDuckGo/BrowserTab/View/WebView.swift index a4b66fe3f2..61cbf039e6 100644 --- a/DuckDuckGo/BrowserTab/View/WebView.swift +++ b/DuckDuckGo/BrowserTab/View/WebView.swift @@ -146,8 +146,7 @@ final class WebView: WKWebView { } var isInspectorShown: Bool { - guard let result = inspectorPerform("isVisible") else { return false } - return result.toOpaque() == UnsafeMutableRawPointer(bitPattern: 0x1) + return inspectorPerform("isVisible") != nil } @nonobjc func openDeveloperTools() { From 63a3d350fa0fe2ed045325fb76bf03ac44978a24 Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Sun, 16 Jan 2022 22:30:11 +0100 Subject: [PATCH 6/9] Favicons (#388) * Refactored to FaviconManager * Using 16x16 icon for tab bar favicons as the last option * Burning * Burning exceptions - bookmarks * Regular cleaning of old favicons * Prevent initial content setting to trigger storing of the state * Loading of favicons after app finished launching. + Unit tests of burning fixed for no favicons * Refreshing favicons in bookmark views * Small cropping of the favicon on homepage disabled * Making sure small images are used if medium aren't available * Using URLSession.dataTask instead of NSImage(contentsOf:) * Burning exception of bookmarked domains --- DuckDuckGo.xcodeproj/project.pbxproj | 88 ++++- DuckDuckGo/AppDelegate/AppDelegate.swift | 1 + DuckDuckGo/Bookmarks/Model/Bookmark.swift | 20 +- DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 2 +- .../Bookmarks/Model/BookmarkManager.swift | 46 +-- .../Bookmarks/Services/BookmarkStore.swift | 2 - .../View/BookmarkListViewController.swift | 6 + ...kmarkManagementSidebarViewController.swift | 2 + .../View/BookmarkOutlineViewCell.swift | 2 +- .../View/BookmarkTableCellView.swift | 2 +- .../ViewModel/BookmarkViewModel.swift | 2 +- DuckDuckGo/BrowserTab/Model/Tab.swift | 54 +-- .../BrowserTab/Services/FaviconService.swift | 141 ------- .../Common/Extensions/NSImageExtensions.swift | 8 + .../Extensions/URLSessionExtension.swift | 2 +- DuckDuckGo/Common/Utilities/Logging.swift | 7 + .../View/SwiftUI/LoginFaviconView.swift | 4 +- DuckDuckGo/Favicons/Model/Favicon.swift | 112 ++++++ .../Favicons/Model/FaviconHostReference.swift | 30 ++ .../Favicons/Model/FaviconImageCache.swift | 155 ++++++++ .../Favicons/Model/FaviconManager.swift | 241 ++++++++++++ .../Model/FaviconReferenceCache.swift | 318 ++++++++++++++++ .../Favicons/Model/FaviconSelector.swift | 61 +++ .../Favicons/Model/FaviconSize.swift | 22 +- .../Favicons/Model/FaviconUrlReference.swift | 29 ++ .../Model/FaviconUserScript.swift | 41 ++- .../Favicons/Services/FaviconStore.swift | 348 ++++++++++++++++++ .../Favicons.xcdatamodel/contents | 31 ++ DuckDuckGo/Fire/Model/Fire.swift | 32 +- .../Fire/View/FirePopoverViewController.swift | 4 +- .../Fire/ViewModel/FirePopoverViewModel.swift | 12 +- .../History 2.xcdatamodel/contents | 2 +- .../View/HomepageCollectionViewItem.swift | 2 +- .../View/HomepageCollectionViewItem.xib | 14 +- .../View/NavigationButtonMenuDelegate.swift | 2 +- .../WKBackForwardListItemViewModel.swift | 16 +- .../View/FireproofDomainsViewController.swift | 4 +- .../View/SaveCredentialsViewController.swift | 4 +- .../AppStateRestorationManager.swift | 7 +- .../Statistics/ATB/VariantManager.swift | 2 +- .../App/AppStateChangePublisherTests.swift | 5 +- .../Bookmarks/Model/BookmarkListTests.swift | 15 +- .../BookmarkOutlineViewDataSourceTests.swift | 21 +- .../BookmarkSidebarTreeControllerTests.swift | 13 +- .../Model/LocalBookmarkManagerTests.swift | 32 +- .../Services/LocalBookmarkStoreTests.swift | 8 +- .../Services/FaviconManagerMock.swift | 53 +++ .../ViewModel/TabViewModelTests.swift | 15 - Unit Tests/Fire/Model/FireTests.swift | 14 +- .../Statistics/PixelArgumentsTests.swift | 2 +- 50 files changed, 1689 insertions(+), 367 deletions(-) delete mode 100644 DuckDuckGo/BrowserTab/Services/FaviconService.swift create mode 100644 DuckDuckGo/Favicons/Model/Favicon.swift create mode 100644 DuckDuckGo/Favicons/Model/FaviconHostReference.swift create mode 100644 DuckDuckGo/Favicons/Model/FaviconImageCache.swift create mode 100644 DuckDuckGo/Favicons/Model/FaviconManager.swift create mode 100644 DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift create mode 100644 DuckDuckGo/Favicons/Model/FaviconSelector.swift rename Unit Tests/BrowserTab/Services/FaviconServiceMock.swift => DuckDuckGo/Favicons/Model/FaviconSize.swift (51%) create mode 100644 DuckDuckGo/Favicons/Model/FaviconUrlReference.swift rename DuckDuckGo/{BrowserTab => Favicons}/Model/FaviconUserScript.swift (60%) create mode 100644 DuckDuckGo/Favicons/Services/FaviconStore.swift create mode 100644 DuckDuckGo/Favicons/Services/Favicons.xcdatamodeld/Favicons.xcdatamodel/contents create mode 100644 Unit Tests/BrowserTab/Services/FaviconManagerMock.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 00eb74d919..aea6270cd6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -294,6 +294,7 @@ AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */; }; AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */; }; AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13DCB3271480B0006D48D3 /* FirePopoverViewModel.swift */; }; + AA222CB92760F74E00321475 /* FaviconReferenceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA222CB82760F74E00321475 /* FaviconReferenceCache.swift */; }; AA2CB12D2587BB5600AA6FBE /* TabBarFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */; }; AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2CB1342587C29500AA6FBE /* TabBarFooter.swift */; }; AA34396C2754D4E300B241FA /* shield.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396A2754D4E200B241FA /* shield.json */; }; @@ -310,7 +311,7 @@ AA4BBA3B25C58FA200C4FB0F /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */; }; AA4D700725545EF800C3411E /* URLEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D700625545EF800C3411E /* URLEventHandler.swift */; }; AA4FF40C2624751A004E2377 /* GrammarFeaturesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4FF40B2624751A004E2377 /* GrammarFeaturesManager.swift */; }; - AA512D1424D99D9800230283 /* FaviconService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA512D1324D99D9800230283 /* FaviconService.swift */; }; + AA512D1424D99D9800230283 /* FaviconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA512D1324D99D9800230283 /* FaviconManager.swift */; }; AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA585D81248FD31100E9A3E2 /* AppDelegate.swift */; }; AA585D84248FD31100E9A3E2 /* BrowserTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */; }; AA585D86248FD31400E9A3E2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; @@ -320,6 +321,12 @@ AA5C8F5E2590EEE800748EB7 /* NSPointExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F5D2590EEE800748EB7 /* NSPointExtension.swift */; }; AA5C8F632591021700748EB7 /* NSApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */; }; AA5D6DAC24A340F700C6FBCE /* WebViewStateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5D6DAB24A340F700C6FBCE /* WebViewStateObserver.swift */; }; + AA5FA697275F90C400DCE9C9 /* FaviconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA696275F90C400DCE9C9 /* FaviconImageCache.swift */; }; + AA5FA69A275F91C700DCE9C9 /* Favicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA699275F91C700DCE9C9 /* Favicon.swift */; }; + AA5FA69D275F945C00DCE9C9 /* FaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */; }; + AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69E275F948900DCE9C9 /* Favicons.xcdatamodeld */; }; + AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6197C3276B314D008396F0 /* FaviconUrlReference.swift */; }; + AA6197C6276B3168008396F0 /* FaviconHostReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6197C5276B3168008396F0 /* FaviconHostReference.swift */; }; AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA61C0CF2722159B00E6B681 /* FireInfoViewController.swift */; }; AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */; }; AA63745424C9BF9A00AB2AC4 /* SuggestionContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA63745324C9BF9A00AB2AC4 /* SuggestionContainerTests.swift */; }; @@ -385,7 +392,7 @@ AAB7320726DD0C37002FACF9 /* Fire.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAB7320626DD0C37002FACF9 /* Fire.storyboard */; }; AAB7320926DD0CD9002FACF9 /* FireViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB7320826DD0CD9002FACF9 /* FireViewController.swift */; }; AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; - AABAF59C260A7D130085060C /* FaviconServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABAF59B260A7D130085060C /* FaviconServiceMock.swift */; }; + AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABAF59B260A7D130085060C /* FaviconManagerMock.swift */; }; AABEE69A24A902A90043105B /* SuggestionContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEE69924A902A90043105B /* SuggestionContainerViewModel.swift */; }; AABEE69C24A902BB0043105B /* SuggestionContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEE69B24A902BB0043105B /* SuggestionContainer.swift */; }; AABEE6A524AA0A7F0043105B /* SuggestionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEE6A424AA0A7F0043105B /* SuggestionViewController.swift */; }; @@ -441,6 +448,7 @@ AAEC74BC2642F0F800C2EFBC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; }; AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAECA41F24EEA4AC00EFA63A /* IndexPathExtension.swift */; }; AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEEC6A827088ADB008445F7 /* FireCoordinator.swift */; }; + AAEF6BC8276A081C0024DCF4 /* FaviconSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEF6BC7276A081C0024DCF4 /* FaviconSelector.swift */; }; AAF7D3862567CED500998667 /* WebViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF7D3852567CED500998667 /* WebViewConfiguration.swift */; }; AAFCB37F25E545D400859DD4 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */; }; AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */; }; @@ -939,6 +947,7 @@ AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProviderTests.swift; sourceTree = ""; }; AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionLoadingMock.swift; sourceTree = ""; }; AA13DCB3271480B0006D48D3 /* FirePopoverViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewModel.swift; sourceTree = ""; }; + AA222CB82760F74E00321475 /* FaviconReferenceCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconReferenceCache.swift; sourceTree = ""; }; AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TabBarFooter.xib; sourceTree = ""; }; AA2CB1342587C29500AA6FBE /* TabBarFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarFooter.swift; sourceTree = ""; }; AA34396A2754D4E200B241FA /* shield.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = shield.json; sourceTree = ""; }; @@ -955,7 +964,7 @@ AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = ""; }; AA4D700625545EF800C3411E /* URLEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLEventHandler.swift; sourceTree = ""; }; AA4FF40B2624751A004E2377 /* GrammarFeaturesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrammarFeaturesManager.swift; sourceTree = ""; }; - AA512D1324D99D9800230283 /* FaviconService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconService.swift; sourceTree = ""; }; + AA512D1324D99D9800230283 /* FaviconManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconManager.swift; sourceTree = ""; }; AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DuckDuckGo.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA585D81248FD31100E9A3E2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserTabViewController.swift; sourceTree = ""; }; @@ -970,6 +979,12 @@ AA5C8F5D2590EEE800748EB7 /* NSPointExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPointExtension.swift; sourceTree = ""; }; AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSApplicationExtension.swift; sourceTree = ""; }; AA5D6DAB24A340F700C6FBCE /* WebViewStateObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewStateObserver.swift; sourceTree = ""; }; + AA5FA696275F90C400DCE9C9 /* FaviconImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconImageCache.swift; sourceTree = ""; }; + AA5FA699275F91C700DCE9C9 /* Favicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favicon.swift; sourceTree = ""; }; + AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconStore.swift; sourceTree = ""; }; + AA5FA69F275F948900DCE9C9 /* Favicons.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Favicons.xcdatamodel; sourceTree = ""; }; + AA6197C3276B314D008396F0 /* FaviconUrlReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconUrlReference.swift; sourceTree = ""; }; + AA6197C5276B3168008396F0 /* FaviconHostReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconHostReference.swift; sourceTree = ""; }; AA61C0CF2722159B00E6B681 /* FireInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireInfoViewController.swift; sourceTree = ""; }; AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; AA63745324C9BF9A00AB2AC4 /* SuggestionContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionContainerTests.swift; sourceTree = ""; }; @@ -1036,7 +1051,7 @@ AAB7320626DD0C37002FACF9 /* Fire.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Fire.storyboard; sourceTree = ""; }; AAB7320826DD0CD9002FACF9 /* FireViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireViewController.swift; sourceTree = ""; }; AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionListCharacteristics.swift; sourceTree = ""; }; - AABAF59B260A7D130085060C /* FaviconServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconServiceMock.swift; sourceTree = ""; }; + AABAF59B260A7D130085060C /* FaviconManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconManagerMock.swift; sourceTree = ""; }; AABEE69924A902A90043105B /* SuggestionContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionContainerViewModel.swift; sourceTree = ""; }; AABEE69B24A902BB0043105B /* SuggestionContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionContainer.swift; sourceTree = ""; }; AABEE6A424AA0A7F0043105B /* SuggestionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionViewController.swift; sourceTree = ""; }; @@ -1092,6 +1107,7 @@ AAEC74BA2642E67C00C2EFBC /* NSPersistentContainerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPersistentContainerExtension.swift; sourceTree = ""; }; AAECA41F24EEA4AC00EFA63A /* IndexPathExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexPathExtension.swift; sourceTree = ""; }; AAEEC6A827088ADB008445F7 /* FireCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireCoordinator.swift; sourceTree = ""; }; + AAEF6BC7276A081C0024DCF4 /* FaviconSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconSelector.swift; sourceTree = ""; }; AAF7D3852567CED500998667 /* WebViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewConfiguration.swift; sourceTree = ""; }; AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherExtension.swift; sourceTree = ""; }; AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProvider.swift; sourceTree = ""; }; @@ -2213,7 +2229,6 @@ AA512D1224D99D4900230283 /* Services */ = { isa = PBXGroup; children = ( - AA512D1324D99D9800230283 /* FaviconService.swift */, AA6820E325502F19005ED0D5 /* WebsiteDataStore.swift */, ); path = Services; @@ -2257,12 +2272,13 @@ 4B723DEA26B0002B00E14D75 /* Data Import */, 4B723DF826B0002B00E14D75 /* Data Export */, 4B65143C26392483005B46EB /* Email */, - B65536902684409300085A79 /* Geolocation */, - 0230C09D271F52D50018F728 /* GPC */, + AA5FA695275F823900DCE9C9 /* Favicons */, 8556A60C256C15C60092FA9D /* FileDownload */, 85A0115D25AF1C4700FA6A0C /* FindInPage */, AA6820E825503A21005ED0D5 /* Fire */, 4B02197B25E05FAC00ED7DEA /* Fireproofing */, + B65536902684409300085A79 /* Geolocation */, + 0230C09D271F52D50018F728 /* GPC */, AAE75275263B036300B973F8 /* History */, AAE71DB225F66A0900D74437 /* Homepage */, AA585DB02490E6FA00E9A3E2 /* Main */, @@ -2336,6 +2352,39 @@ path = Main; sourceTree = ""; }; + AA5FA695275F823900DCE9C9 /* Favicons */ = { + isa = PBXGroup; + children = ( + AA5FA698275F90CD00DCE9C9 /* Model */, + AA5FA69B275F944500DCE9C9 /* Services */, + ); + path = Favicons; + sourceTree = ""; + }; + AA5FA698275F90CD00DCE9C9 /* Model */ = { + isa = PBXGroup; + children = ( + AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */, + AA512D1324D99D9800230283 /* FaviconManager.swift */, + AAEF6BC7276A081C0024DCF4 /* FaviconSelector.swift */, + AA5FA696275F90C400DCE9C9 /* FaviconImageCache.swift */, + AA222CB82760F74E00321475 /* FaviconReferenceCache.swift */, + AA6197C5276B3168008396F0 /* FaviconHostReference.swift */, + AA6197C3276B314D008396F0 /* FaviconUrlReference.swift */, + AA5FA699275F91C700DCE9C9 /* Favicon.swift */, + ); + path = Model; + sourceTree = ""; + }; + AA5FA69B275F944500DCE9C9 /* Services */ = { + isa = PBXGroup; + children = ( + AA5FA69E275F948900DCE9C9 /* Favicons.xcdatamodeld */, + AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */, + ); + path = Services; + sourceTree = ""; + }; AA63744E24C9BB4A00AB2AC4 /* Suggestions */ = { isa = PBXGroup; children = ( @@ -2589,7 +2638,6 @@ 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */, 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */, 85E11C2E25E7DC7E00974CAF /* ExternalURLHandler.swift */, - AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */, AA9FF95824A1ECF20039E328 /* Tab.swift */, 85AC3AEE25D5CE9800C7D2AA /* UserScripts.swift */, AAF7D3852567CED500998667 /* WebViewConfiguration.swift */, @@ -2693,7 +2741,7 @@ children = ( 4B0219A725E0646500ED7DEA /* WebsiteDataStoreTests.swift */, AA9C362725518C44004B1BA3 /* WebsiteDataStoreMock.swift */, - AABAF59B260A7D130085060C /* FaviconServiceMock.swift */, + AABAF59B260A7D130085060C /* FaviconManagerMock.swift */, ); path = Services; sourceTree = ""; @@ -3729,11 +3777,13 @@ 85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */, B6A9E48426146AAB0067D1B9 /* PixelParameters.swift in Sources */, 4B0511BF262CAA5A00F6079C /* PreferenceSections.swift in Sources */, + AA5FA697275F90C400DCE9C9 /* FaviconImageCache.swift in Sources */, 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */, 4B92929B26670D2A00AD2C21 /* BookmarkOutlineViewDataSource.swift in Sources */, 85D885B026A590A90077C374 /* NSNotificationName+PasswordManager.swift in Sources */, AAC30A28268E045400D2D9CD /* CrashReportReader.swift in Sources */, 85AC3B3525DA82A600C7D2AA /* DataTaskProviding.swift in Sources */, + AAEF6BC8276A081C0024DCF4 /* FaviconSelector.swift in Sources */, 4B2E7D6326FF9D6500D2DB17 /* PrintingUserScript.swift in Sources */, 0230C0A52721F3750018F728 /* GPCRequestFactory.swift in Sources */, 4BA1A6B3258B080A00F6F690 /* EncryptionKeyGeneration.swift in Sources */, @@ -3783,11 +3833,12 @@ 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, AAFCB37F25E545D400859DD4 /* PublisherExtension.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, + AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */, AAC30A2A268E239100D2D9CD /* CrashReport.swift in Sources */, 4B78A86B26BB3ADD0071BB16 /* BrowserImportSummaryViewController.swift in Sources */, - AA512D1424D99D9800230283 /* FaviconService.swift in Sources */, + AA512D1424D99D9800230283 /* FaviconManager.swift in Sources */, AABEE6AB24ACA0F90043105B /* SuggestionTableRowView.swift in Sources */, 4B0511CB262CAA5A00F6079C /* DownloadPreferencesTableCellView.swift in Sources */, 4B9292AA26670D3700AD2C21 /* Bookmark.xcmappingmodel in Sources */, @@ -3820,6 +3871,7 @@ 4B9292DB2667125D00AD2C21 /* ContextualMenu.swift in Sources */, AA68C3D32490ED62001B8783 /* NavigationBarViewController.swift in Sources */, AA585DAF2490E6E600E9A3E2 /* MainViewController.swift in Sources */, + AA5FA69A275F91C700DCE9C9 /* Favicon.swift in Sources */, AABEE69A24A902A90043105B /* SuggestionContainerViewModel.swift in Sources */, AA840A9827319D1600E63CDD /* FirePopoverWrapperViewController.swift in Sources */, B657841F25FA497600D8DB33 /* NSException+Catch.swift in Sources */, @@ -3845,12 +3897,14 @@ B6B1E87B26D381710062C350 /* DownloadListCoordinator.swift in Sources */, AAC5E4F125D6BF10007F5990 /* AddressBarButton.swift in Sources */, AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */, + AA5FA69D275F945C00DCE9C9 /* FaviconStore.swift in Sources */, AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */, AAADFD06264AA282001555EA /* TimeIntervalExtension.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, 4B9292D32667123700AD2C21 /* AddBookmarkModalViewController.swift in Sources */, AA88D14B252A557100980B4E /* URLRequestExtension.swift in Sources */, + AA6197C6276B3168008396F0 /* FaviconHostReference.swift in Sources */, 4B8AC93B26B48ADF00879451 /* ASN1Parser.swift in Sources */, 336B39E72726BAE800C417D3 /* UserDefaultsExtension.swift in Sources */, B66E9DD22670EB2A00E53BB5 /* _WKDownload+WebKitDownload.swift in Sources */, @@ -3872,6 +3926,7 @@ B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, + AA222CB92760F74E00321475 /* FaviconReferenceCache.swift in Sources */, B6C0B24426E9CB080031CB7F /* RunLoopExtension.swift in Sources */, 4B9292A126670D2A00AD2C21 /* BookmarkTreeController.swift in Sources */, 4B9292D02667123700AD2C21 /* BookmarkManagementSplitViewController.swift in Sources */, @@ -3933,6 +3988,7 @@ B657841A25FA484B00D8DB33 /* NSException+Catch.m in Sources */, B684592F25C93FBF00DC17B6 /* AppStateRestorationManager.swift in Sources */, AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */, + AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */, AAC5E4C825D6A6E8007F5990 /* BookmarkPopoverViewController.swift in Sources */, 85CC1D7B26A05ECF0062F04E /* PasswordManagementItemListModel.swift in Sources */, AABEE6A924AB4B910043105B /* SuggestionTableCellView.swift in Sources */, @@ -4190,7 +4246,7 @@ 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, 4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */, AAEC74BB2642E67C00C2EFBC /* NSPersistentContainerExtension.swift in Sources */, - AABAF59C260A7D130085060C /* FaviconServiceMock.swift in Sources */, + AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */, 4BA1A6E6258C270800F6F690 /* EncryptionKeyGeneratorTests.swift in Sources */, B6106BB326A7F4AA0013B453 /* GeolocationServiceMock.swift in Sources */, @@ -5249,6 +5305,16 @@ sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; + AA5FA69E275F948900DCE9C9 /* Favicons.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + AA5FA69F275F948900DCE9C9 /* Favicons.xcdatamodel */, + ); + currentVersion = AA5FA69F275F948900DCE9C9 /* Favicons.xcdatamodel */; + path = Favicons.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; AAE75278263B046100B973F8 /* History.xcdatamodeld */ = { isa = XCVersionGroup; children = ( diff --git a/DuckDuckGo/AppDelegate/AppDelegate.swift b/DuckDuckGo/AppDelegate/AppDelegate.swift index 39e829020d..f88b7cf40c 100644 --- a/DuckDuckGo/AppDelegate/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate/AppDelegate.swift @@ -79,6 +79,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { HTTPSUpgrade.shared.loadDataAsync() LocalBookmarkManager.shared.loadBookmarks() + FaviconManager.shared.loadFavicons() _=ConfigurationManager.shared _=DownloadListCoordinator.shared diff --git a/DuckDuckGo/Bookmarks/Model/Bookmark.swift b/DuckDuckGo/Bookmarks/Model/Bookmark.swift index 8784f7d4ff..773127c82d 100644 --- a/DuckDuckGo/Bookmarks/Model/Bookmark.swift +++ b/DuckDuckGo/Bookmarks/Model/Bookmark.swift @@ -80,7 +80,7 @@ internal class BaseBookmarkEntity { return Bookmark(id: id, url: url, title: title, - favicon: managedObject.faviconEncrypted as? NSImage, + oldFavicon: managedObject.faviconEncrypted as? NSImage, isFavorite: managedObject.isFavorite, parentFolderUUID: parentFolderUUID) } @@ -134,21 +134,29 @@ final class Bookmark: BaseBookmarkEntity { } let url: URL - var favicon: NSImage? var isFavorite: Bool var parentFolderUUID: UUID? + // Property oldFavicon can be removed in future updates when favicon cache is built + var oldFavicon: NSImage? + let faviconManagement: FaviconManagement + func favicon(_ sizeCategory: Favicon.SizeCategory) -> NSImage? { + return faviconManagement.getCachedFavicon(for: url, sizeCategory: sizeCategory)?.image ?? oldFavicon + } + init(id: UUID, url: URL, title: String, - favicon: NSImage? = nil, + oldFavicon: NSImage? = nil, isFavorite: Bool, - parentFolderUUID: UUID? = nil) { + parentFolderUUID: UUID? = nil, + faviconManagement: FaviconManagement = FaviconManager.shared) { self.url = url - self.favicon = favicon + self.oldFavicon = oldFavicon self.isFavorite = isFavorite self.parentFolderUUID = parentFolderUUID + self.faviconManagement = faviconManagement super.init(id: id, title: title, isFolder: false) } @@ -157,7 +165,7 @@ final class Bookmark: BaseBookmarkEntity { self.init(id: bookmark.id, url: newUrl, title: bookmark.title, - favicon: nil, + oldFavicon: nil, isFavorite: bookmark.isFavorite) } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 149ea700fb..bf1bb208b5 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -23,7 +23,7 @@ struct BookmarkList { var topLevelEntities: [BaseBookmarkEntity] = [] - private var allBookmarkURLsOrdered: [URL] + private(set) var allBookmarkURLsOrdered: [URL] private var favoriteBookmarkURLsOrdered: [URL] private var itemsDict: [URL: Bookmark] diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index 9a0cac5584..19192df95d 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -23,6 +23,7 @@ import Combine protocol BookmarkManager: AnyObject { func isUrlBookmarked(url: URL) -> Bool + func isHostInBookmarks(host: String) -> Bool func getBookmark(for url: URL) -> Bookmark? @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? @discardableResult func makeFolder(for title: String, parent: BookmarkFolder?) -> BookmarkFolder @@ -45,22 +46,18 @@ final class LocalBookmarkManager: BookmarkManager { static let shared = LocalBookmarkManager() - private init() { - subscribeToCachedFavicons() - } + private init() {} - init(bookmarkStore: BookmarkStore, faviconService: FaviconService) { + init(bookmarkStore: BookmarkStore, faviconManagement: FaviconManagement) { self.bookmarkStore = bookmarkStore - self.faviconService = faviconService - - subscribeToCachedFavicons() + self.faviconManagement = faviconManagement } @Published private(set) var list: BookmarkList? var listPublisher: Published.Publisher { $list } private lazy var bookmarkStore: BookmarkStore = LocalBookmarkStore() - private lazy var faviconService: FaviconService = LocalFaviconService.shared + private lazy var faviconManagement: FaviconManagement = FaviconManager.shared // MARK: - Bookmarks @@ -86,6 +83,12 @@ final class LocalBookmarkManager: BookmarkManager { return list?[url] != nil } + func isHostInBookmarks(host: String) -> Bool { + return list?.allBookmarkURLsOrdered.contains(where: { url in + url.host == host + }) ?? false + } + func getBookmark(for url: URL) -> Bookmark? { return list?[url] } @@ -99,7 +102,7 @@ final class LocalBookmarkManager: BookmarkManager { } let id = UUID() - let bookmark = Bookmark(id: id, url: url, title: title, favicon: favicon(for: url.host), isFavorite: isFavorite) + let bookmark = Bookmark(id: id, url: url, title: title, isFavorite: isFavorite) list?.insert(bookmark) bookmarkStore.save(bookmark: bookmark, parent: nil) { [weak self] success, _ in @@ -201,32 +204,9 @@ final class LocalBookmarkManager: BookmarkManager { // MARK: - Favicons - private var faviconCancellable: AnyCancellable? - - private func subscribeToCachedFavicons() { - faviconCancellable = faviconService.cachedFaviconsPublisher - .sink(receiveValue: { [weak self] (host, favicon) in - self?.update(favicon: favicon, for: host) - }) - } - - private func update(favicon: NSImage, for host: String) { - guard let bookmarks = list?.bookmarks() else { return } - - bookmarks - .filter { $0.url.host == host && - $0.favicon?.size.isSmaller(than: favicon.size) ?? true - } - .forEach { - let bookmark = $0 - bookmark.favicon = favicon - update(bookmark: bookmark) - } - } - private func favicon(for host: String?) -> NSImage? { if let host = host { - return faviconService.getCachedFavicon(for: host, mustBeFromUserScript: false) + return faviconManagement.getCachedFavicon(for: host, sizeCategory: .small)?.image } return nil diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 32424a000d..00435b3cd8 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -122,7 +122,6 @@ final class LocalBookmarkStore: BookmarkStore { bookmarkMO.id = bookmark.id bookmarkMO.urlEncrypted = bookmark.url as NSURL? bookmarkMO.titleEncrypted = bookmark.title as NSString - bookmarkMO.faviconEncrypted = bookmark.favicon bookmarkMO.isFavorite = bookmark.isFavorite bookmarkMO.isFolder = bookmark.isFolder bookmarkMO.dateAdded = NSDate.now @@ -485,7 +484,6 @@ fileprivate extension BookmarkManagedObject { id = bookmark.id urlEncrypted = bookmark.url as NSURL? titleEncrypted = bookmark.title as NSString - faviconEncrypted = bookmark.favicon isFavorite = bookmark.isFavorite isFolder = false } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 998985850e..e6a00a7775 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -82,6 +82,12 @@ final class BookmarkListViewController: NSViewController { }.store(in: &cancellables) } + override func viewWillAppear() { + super.viewWillAppear() + + reloadData() + } + private func reloadData() { let selectedNodes = self.selectedNodes diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index e297c08b6a..ae1e0d1ded 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -105,6 +105,8 @@ final class BookmarkManagementSidebarViewController: NSViewController { override func viewWillAppear() { super.viewWillAppear() + reloadData() + tabSwitcherButton.select(tabType: .bookmarks) } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.swift b/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.swift index 988b63d140..4f500477c2 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkOutlineViewCell.swift @@ -48,7 +48,7 @@ final class BookmarkOutlineViewCell: NSTableCellView { private func commonInit() {} func update(from bookmark: Bookmark) { - faviconImageView.image = bookmark.favicon ?? Self.defaultBookmarkFavicon + faviconImageView.image = bookmark.favicon(.small) ?? Self.defaultBookmarkFavicon titleLabel.stringValue = bookmark.title countLabel.stringValue = "" } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index 893d54a5e5..ab80c25273 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -169,7 +169,7 @@ final class BookmarkTableCellView: NSTableCellView, NibLoadable { func update(from bookmark: Bookmark) { self.entity = bookmark - faviconImageView.image = bookmark.favicon ?? Self.defaultBookmarkFavicon + faviconImageView.image = bookmark.favicon(.small) accessoryImageView.image = bookmark.isFavorite ? Self.favoriteAccessoryViewImage : nil favoriteButton.image = bookmark.isFavorite ? Self.favoriteFilledAccessoryViewImage : Self.favoriteAccessoryViewImage primaryTitleLabelValue = bookmark.title diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift index b686fb285d..af9aa84454 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift @@ -47,7 +47,7 @@ struct BookmarkViewModel { // bookmark.isFavorite ? bookmark.favicon?.makeFavoriteOverlay() : bookmark.favicon if let bookmark = entity as? Bookmark { - let favicon = bookmark.favicon?.copy() as? NSImage + let favicon = bookmark.favicon(.small)?.copy() as? NSImage favicon?.size = NSSize.faviconSize return favicon } else if entity is BookmarkFolder { diff --git a/DuckDuckGo/BrowserTab/Model/Tab.swift b/DuckDuckGo/BrowserTab/Model/Tab.swift index 80d12d46b7..578297d9a4 100644 --- a/DuckDuckGo/BrowserTab/Model/Tab.swift +++ b/DuckDuckGo/BrowserTab/Model/Tab.swift @@ -90,7 +90,7 @@ final class Tab: NSObject { weak var delegate: TabDelegate? init(content: TabContent, - faviconService: FaviconService = LocalFaviconService.shared, + faviconManagement: FaviconManagement = FaviconManager.shared, webCacheManager: WebCacheManager = WebCacheManager.shared, webViewConfiguration: WebViewConfiguration? = nil, historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, @@ -105,7 +105,7 @@ final class Tab: NSObject { canBeClosedWithBack: Bool = false) { self.content = content - self.faviconService = faviconService + self.faviconManagement = faviconManagement self.historyCoordinating = historyCoordinating self.scriptsSource = scriptsSource self.visitedDomains = visitedDomains @@ -125,12 +125,6 @@ final class Tab: NSObject { super.init() setupWebView(shouldLoadInBackground: shouldLoadInBackground) - - // cache session-restored favicon if present - if let favicon = favicon, - let host = content.url?.host { - faviconService.cacheIfNeeded(favicon: favicon, for: host, isFromUserScript: false) - } } deinit { @@ -382,32 +376,22 @@ final class Tab: NSObject { // MARK: - Favicon @Published var favicon: NSImage? - let faviconService: FaviconService + let faviconManagement: FaviconManagement private func handleFavicon(oldContent: TabContent) { - if !content.isUrl { - favicon = nil - } - if oldContent.url?.host != content.url?.host { - fetchFavicon(nil, for: content.url?.host, isFromUserScript: false) - } - } + guard faviconManagement.areFaviconsLoaded else { return } - private func fetchFavicon(_ faviconURL: URL?, for host: String?, isFromUserScript: Bool) { - if favicon != nil { + guard content.isUrl, let url = content.url else { favicon = nil - } - - guard let host = host else { return } - faviconService.fetchFavicon(faviconURL, for: host, isFromUserScript: isFromUserScript) { (image, error) in - guard error == nil, let image = image else { - return + if let cachedFavicon = faviconManagement.getCachedFavicon(for: url, sizeCategory: .small)?.image { + if cachedFavicon != favicon { + favicon = cachedFavicon } - - self.favicon = image + } else { + favicon = nil } } @@ -588,20 +572,14 @@ extension Tab: ContextMenuDelegate { extension Tab: FaviconUserScriptDelegate { - func faviconUserScript(_ faviconUserScript: FaviconUserScript, didFindFavicon faviconUrl: URL) { - guard let host = self.content.url?.host else { - return - } - - faviconService.fetchFavicon(faviconUrl, for: host, isFromUserScript: true) { (image, error) in - guard host == self.content.url?.host else { - return - } - guard error == nil, let image = image else { + func faviconUserScript(_ faviconUserScript: FaviconUserScript, + didFindFaviconLinks faviconLinks: [FaviconUserScript.FaviconLink], + for documentUrl: URL) { + faviconManagement.handleFaviconLinks(faviconLinks, documentUrl: documentUrl) { favicon in + guard documentUrl == self.content.url, let favicon = favicon else { return } - - self.favicon = image + self.favicon = favicon.image } } diff --git a/DuckDuckGo/BrowserTab/Services/FaviconService.swift b/DuckDuckGo/BrowserTab/Services/FaviconService.swift deleted file mode 100644 index 2b2e268e34..0000000000 --- a/DuckDuckGo/BrowserTab/Services/FaviconService.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// FaviconService.swift -// -// Copyright © 2020 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Cocoa -import Combine - -protocol FaviconService { - - var cachedFaviconsPublisher: PassthroughSubject<(host: String, favicon: NSImage), Never> { get } - - func fetchFavicon(_ faviconUrl: URL?, for host: String, isFromUserScript: Bool, completion: @escaping (NSImage?, Error?) -> Void) - func getCachedFavicon(for host: String, mustBeFromUserScript: Bool) -> NSImage? - func cacheIfNeeded(favicon: NSImage, for host: String, isFromUserScript: Bool) - -} - -final class LocalFaviconService: FaviconService { - - static let shared = LocalFaviconService() - - var cachedFaviconsPublisher = PassthroughSubject<(host: String, favicon: NSImage), Never>() - - private enum FaviconName { - static let favicon = "favicon.ico" - } - - private enum Favicon { - static let duckDuckGo = NSImage(named: "HomeFavicon")! - } - - private struct CacheEntry { - let image: NSImage - let isFromUserScript: Bool - } - - private var cache = [String: CacheEntry]() - private let queue = DispatchQueue(label: "LocalFaviconService queue", attributes: .concurrent) - - enum LocalFaviconServiceError: Error { - case urlConstructionFailed - case imageInitFailed - } - - init() { - initCache() - - DistributedNotificationCenter.default.addObserver( - self, - selector: #selector(themeChanged), - name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), - object: nil) - } - - private func initCache() { - queue.async(flags: .barrier) { - if let duckduckgoHost = URL.duckDuckGo.host { - self.cache[duckduckgoHost] = CacheEntry(image: Favicon.duckDuckGo, isFromUserScript: true) - } - } - } - - func fetchFavicon(_ faviconUrl: URL?, for host: String, isFromUserScript: Bool, completion: @escaping (NSImage?, Error?) -> Void) { - - func mainQueueCompletion(_ favicon: NSImage?, _ error: Error?) { - DispatchQueue.main.async { - completion(favicon, error) - } - } - - queue.async { - if let cachedFavicon = self.getCachedFavicon(for: host, mustBeFromUserScript: isFromUserScript) { - mainQueueCompletion(cachedFavicon, nil) - return - } - - guard let url = faviconUrl ?? URL(string: "\(URL.NavigationalScheme.https.separated())\(host)/\(FaviconName.favicon)") else { - mainQueueCompletion(nil, LocalFaviconServiceError.urlConstructionFailed) - return - } - - guard let image = NSImage(contentsOf: url), image.isValid else { - if let newHost = host.dropSubdomain(), faviconUrl == nil { - self.fetchFavicon(nil, for: newHost, isFromUserScript: isFromUserScript, completion: completion) - } else { - mainQueueCompletion(nil, LocalFaviconServiceError.imageInitFailed) - } - return - } - - self.cacheIfNeeded(favicon: image, for: host, isFromUserScript: isFromUserScript) - mainQueueCompletion(image, nil) - } - } - - func cacheIfNeeded(favicon: NSImage, for host: String, isFromUserScript: Bool) { - queue.async(flags: .barrier) { - // Don't replace a favicon from the UserScript with one that isn't from the UserScript - if let entry = self.cache[host], - entry.isFromUserScript && !isFromUserScript { - return - } - self.cache[host] = CacheEntry(image: favicon, isFromUserScript: isFromUserScript) - - DispatchQueue.main.async { [weak self] in - self?.cachedFaviconsPublisher.send((host, favicon)) - } - } - } - - func getCachedFavicon(for host: String, mustBeFromUserScript: Bool) -> NSImage? { - guard let entry = cache[host] else { return nil } - if mustBeFromUserScript && !entry.isFromUserScript { - return nil - } - return entry.image - } - - @objc func themeChanged() { - invalidateCache() - } - - private func invalidateCache() { - cache = [String: CacheEntry]() - } - -} diff --git a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift index c067797fad..a4728c1137 100644 --- a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift +++ b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift @@ -35,4 +35,12 @@ extension NSImage { return image } + func resizedToFaviconSize() -> NSImage? { + if size.width > NSSize.faviconSize.width || + size.height > NSSize.faviconSize.height { + return resized(to: .faviconSize) + } + return self + } + } diff --git a/DuckDuckGo/Common/Extensions/URLSessionExtension.swift b/DuckDuckGo/Common/Extensions/URLSessionExtension.swift index e92dd10841..affb97e8ff 100644 --- a/DuckDuckGo/Common/Extensions/URLSessionExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLSessionExtension.swift @@ -23,7 +23,7 @@ extension URLSession { private static var defaultCallbackQueue: OperationQueue = { let queue = OperationQueue() queue.name = "APIRequest default callback queue" - queue.qualityOfService = .utility + queue.qualityOfService = .userInitiated queue.maxConcurrentOperationCount = 1 return queue }() diff --git a/DuckDuckGo/Common/Utilities/Logging.swift b/DuckDuckGo/Common/Utilities/Logging.swift index b01591f5c2..d647f2681d 100644 --- a/DuckDuckGo/Common/Utilities/Logging.swift +++ b/DuckDuckGo/Common/Utilities/Logging.swift @@ -48,6 +48,10 @@ extension OSLog { Logging.contentBlockingLoggingEnabled ? Logging.contentBlockingLog : .disabled } + static var favicons: OSLog { + Logging.faviconLoggingEnabled ? Logging.faviconLog : .disabled + } + } struct Logging { @@ -73,4 +77,7 @@ struct Logging { fileprivate static let contentBlockingLoggingEnabled = false fileprivate static let contentBlockingLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Content Blocking") + fileprivate static let faviconLoggingEnabled = false + fileprivate static let faviconLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Favicons") + } diff --git a/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift index 347c694036..93723b03c5 100644 --- a/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/LoginFaviconView.swift @@ -22,9 +22,11 @@ struct LoginFaviconView: View { let domain: String + let faviconManagement: FaviconManagement = FaviconManager.shared + var body: some View { - let favicon = LocalFaviconService.shared.getCachedFavicon(for: domain, mustBeFromUserScript: false) ?? NSImage(named: "Login") + let favicon = faviconManagement.getCachedFavicon(for: domain, sizeCategory: .small)?.image ?? NSImage(named: "Login") if let image = favicon { Image(nsImage: image) diff --git a/DuckDuckGo/Favicons/Model/Favicon.swift b/DuckDuckGo/Favicons/Model/Favicon.swift new file mode 100644 index 0000000000..767aa58151 --- /dev/null +++ b/DuckDuckGo/Favicons/Model/Favicon.swift @@ -0,0 +1,112 @@ +// +// Favicon.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Cocoa +import Foundation + +struct Favicon { + + enum Relation: Int { + case favicon = 2 + case icon = 1 + case other = 0 + + init(relationString: String) { + if relationString == "favicon" { + self = .favicon + return + } + if relationString.contains("icon") { + self = .icon + return + } + self = .other + } + } + + enum SizeCategory: CGFloat { + case noImage = 0 + case tiny = 1 + case small = 32 + case medium = 132 + case large = 264 + case huge = 2048 + + init(imageSize: CGSize?) { + guard let imageSize = imageSize else { + self = .noImage + return + } + let longestSide = max(imageSize.width, imageSize.height) + switch longestSide { + case 0: self = .noImage + case 1..() + + init(faviconStoring: FaviconStoring) { + storing = faviconStoring + } + + private(set) var loaded = false + + func loadFavicons(completionHandler: ((Error?) -> Void)? = nil) { + storing.loadFavicons() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("Favicons loaded successfully", log: .favicons) + completionHandler?(nil) + case .failure(let error): + os_log("Loading of favicons failed: %s", log: .favicons, type: .error, error.localizedDescription) + completionHandler?(error) + } + }, receiveValue: { [weak self] favicons in + favicons.forEach { favicon in + self?.entries[favicon.url] = favicon + } + self?.loaded = true + }) + .store(in: &self.cancellables) + } + + func insert(_ favicon: Favicon) { + guard loaded else { return } + + // Remove existing favicon with the same URL + if let oldFavicon = entries[favicon.url] { + removeFaviconsFromStore([oldFavicon]) + } + + // Save the new one + entries[favicon.url] = favicon + storing.save(favicon: favicon) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("Favicon saved successfully. URL: %s", log: .favicons, favicon.url.absoluteString) + case .failure(let error): + os_log("Saving of favicon failed: %s", log: .favicons, type: .error, error.localizedDescription) + } + }, receiveValue: {}) + .store(in: &self.cancellables) + } + + func get(faviconUrl: URL) -> Favicon? { + guard loaded else { + return nil + } + + return entries[faviconUrl] + } + + // MARK: - Clean + + func cleanOldExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + removeFavicons(filter: { favicon in + guard let host = favicon.documentUrl.host else { + return false + } + return favicon.dateCreated < Date.monthAgo && + !fireproofDomains.isFireproof(fireproofDomain: host) && + !bookmarkManager.isHostInBookmarks(host: host) + }, completionHandler: completion) + } + + // MARK: - Burning + + func burnExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + removeFavicons(filter: { favicon in + guard let host = favicon.documentUrl.host else { + return false + } + return !(fireproofDomains.isFireproof(fireproofDomain: host) || + bookmarkManager.isHostInBookmarks(host: host)) + }, completionHandler: completion) + } + + func burnDomains(_ domains: Set, + except bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + removeFavicons(filter: { favicon in + guard let host = favicon.documentUrl.host else { + return false + } + return domains.contains(host) && !bookmarkManager.isHostInBookmarks(host: host) + }, completionHandler: completion) + } + + // MARK: - Private + + private func removeFavicons(filter isRemoved: (Favicon) -> Bool, completionHandler: (() -> Void)? = nil) { + let faviconsToRemove = entries.values.filter(isRemoved) + faviconsToRemove.forEach { entries[$0.url] = nil } + + removeFaviconsFromStore(faviconsToRemove, completionHandler: completionHandler) + } + + private func removeFaviconsFromStore(_ favicons: [Favicon], completionHandler: (() -> Void)? = nil) { + guard !favicons.isEmpty else { completionHandler?(); return } + + storing.removeFavicons(favicons) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("Favicons removed successfully.", log: .favicons) + case .failure(let error): + os_log("Removing of favicons failed: %s", log: .favicons, type: .error, error.localizedDescription) + } + completionHandler?() + }, receiveValue: {}) + .store(in: &self.cancellables) + } +} diff --git a/DuckDuckGo/Favicons/Model/FaviconManager.swift b/DuckDuckGo/Favicons/Model/FaviconManager.swift new file mode 100644 index 0000000000..e7e62b323a --- /dev/null +++ b/DuckDuckGo/Favicons/Model/FaviconManager.swift @@ -0,0 +1,241 @@ +// +// FaviconManager.swift +// +// Copyright © 2020 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Cocoa +import Combine +import BrowserServicesKit + +protocol FaviconManagement { + + var areFaviconsLoaded: Bool { get } + func loadFavicons() + + func handleFaviconLinks(_ faviconLinks: [FaviconUserScript.FaviconLink], + documentUrl: URL, + completion: @escaping (Favicon?) -> Void) + func getCachedFavicon(for documentUrl: URL, sizeCategory: Favicon.SizeCategory) -> Favicon? + func getCachedFavicon(for host: String, sizeCategory: Favicon.SizeCategory) -> Favicon? + + func burnExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) + func burnDomains(_ domains: Set, + except bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) + +} + +final class FaviconManager: FaviconManagement { + + static let shared = FaviconManager() + + private lazy var store: FaviconStoring = FaviconStore() + + func loadFavicons() { + imageCache.loadFavicons { _ in + self.imageCache.cleanOldExcept(fireproofDomains: FireproofDomains.shared, + bookmarkManager: LocalBookmarkManager.shared) { + self.referenceCache.loadReferences { _ in + self.referenceCache.cleanOldExcept(fireproofDomains: FireproofDomains.shared, + bookmarkManager: LocalBookmarkManager.shared) + } + } + } + } + + var areFaviconsLoaded: Bool { + imageCache.loaded && referenceCache.loaded + } + + // MARK: - Fetching & Cache + + private lazy var imageCache = FaviconImageCache(faviconStoring: store) + private lazy var referenceCache = FaviconReferenceCache(faviconStoring: store) + + func handleFaviconLinks(_ faviconLinks: [FaviconUserScript.FaviconLink], + documentUrl: URL, + completion: @escaping (Favicon?) -> Void) { + // Manually add favicon.ico into links + let faviconLinks = addingFaviconIco(into: faviconLinks, documentUrl: documentUrl) + + // Fetch favicons if needed + let faviconLinksToFetch = filteringAlreadyFetchedFaviconLinks(from: faviconLinks) + fetchFavicons(faviconLinks: faviconLinksToFetch, documentUrl: documentUrl) { [weak self] newFavicons in + guard let self = self else { return } + + // Insert new favicons to cache + newFavicons.forEach { newFavicon in + self.imageCache.insert(newFavicon) + } + + // Pick most suitable favicons + var cachedFavicons: [Favicon] = faviconLinks + .compactMap { faviconLink -> Favicon? in + guard let faviconUrl = URL(string: faviconLink.href) else { + return nil + } + + if let favicon = self.imageCache.get(faviconUrl: faviconUrl), favicon.dateCreated > Date.weekAgo { + return favicon + } + + return nil + } + + let noFaviconPickedYet = self.referenceCache.getFaviconUrl(for: documentUrl, sizeCategory: .small) == nil + let newFaviconLoaded = !newFavicons.isEmpty + let currentSmallFaviconUrl = self.referenceCache.getFaviconUrl(for: documentUrl, sizeCategory: .small) + let currentMediumFaviconUrl = self.referenceCache.getFaviconUrl(for: documentUrl, sizeCategory: .medium) + let cachedFaviconUrls = cachedFavicons.map {$0.url} + let faviconsOutdated: Bool = { + if let currentSmallFaviconUrl = currentSmallFaviconUrl, !cachedFaviconUrls.contains(currentSmallFaviconUrl) { + return true + } + if let currentMediumFaviconUrl = currentMediumFaviconUrl, !cachedFaviconUrls.contains(currentMediumFaviconUrl) { + return true + } + return false + }() + + // If we haven't pick a favicon yet or there is a new favicon loaded or favicons are outdated + // Pick the most suitable favicons. Otherwise use cached references + if noFaviconPickedYet || newFaviconLoaded || faviconsOutdated { + cachedFavicons = cachedFavicons.sorted(by: { $0.longestSide < $1.longestSide }) + let mediumFavicon = FaviconSelector.getMostSuitableFavicon(for: .medium, favicons: cachedFavicons) + let smallFavicon = FaviconSelector.getMostSuitableFavicon(for: .small, favicons: cachedFavicons) + self.referenceCache.insert(faviconUrls: (smallFavicon?.url, mediumFavicon?.url), documentUrl: documentUrl) + completion(smallFavicon) + } else { + guard let currentSmallFaviconUrl = currentSmallFaviconUrl, + let cachedFavicon = self.imageCache.get(faviconUrl: currentSmallFaviconUrl) else { + completion(nil) + return + } + + completion(cachedFavicon) + } + } + } + + func getCachedFavicon(for documentUrl: URL, sizeCategory: Favicon.SizeCategory) -> Favicon? { + guard let faviconUrl = referenceCache.getFaviconUrl(for: documentUrl, sizeCategory: sizeCategory) else { + return nil + } + + return imageCache.get(faviconUrl: faviconUrl) + } + + func getCachedFavicon(for host: String, sizeCategory: Favicon.SizeCategory) -> Favicon? { + guard let faviconUrl = referenceCache.getFaviconUrl(for: host, sizeCategory: sizeCategory) else { + return nil + } + + return imageCache.get(faviconUrl: faviconUrl) + } + + // MARK: - Burning + + func burnExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + self.referenceCache.burnExcept(fireproofDomains: fireproofDomains, + bookmarkManager: bookmarkManager) { + self.imageCache.burnExcept(fireproofDomains: fireproofDomains, + bookmarkManager: bookmarkManager) { + completion() + } + } + } + + func burnDomains(_ domains: Set, + except bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + self.referenceCache.burnDomains(domains, except: bookmarkManager) { + self.imageCache.burnDomains(domains, except: bookmarkManager) { + DispatchQueue.main.async { + completion() + } + } + } + } + + // MARK: - Private + + private func addingFaviconIco(into faviconLinks: [FaviconUserScript.FaviconLink], documentUrl: URL) -> [FaviconUserScript.FaviconLink] { + var faviconLinks = faviconLinks + if let host = documentUrl.host { + let faviconIcoLink = FaviconUserScript.FaviconLink(href: "\(URL.NavigationalScheme.https.separated())\(host)/favicon.ico", + rel: "favicon.ico") + faviconLinks.append(faviconIcoLink) + } + return faviconLinks + } + + private func filteringAlreadyFetchedFaviconLinks(from faviconLinks: [FaviconUserScript.FaviconLink]) -> [FaviconUserScript.FaviconLink] { + return faviconLinks.filter { faviconLink in + guard let faviconUrl = URL(string: faviconLink.href) else { + return false + } + + if let favicon = imageCache.get(faviconUrl: faviconUrl), favicon.dateCreated > Date.weekAgo { + return false + } else { + return true + } + } + } + + private func fetchFavicons(faviconLinks: [FaviconUserScript.FaviconLink], documentUrl: URL, completion: @escaping ([Favicon]) -> Void) { + guard !faviconLinks.isEmpty else { + completion([]) + return + } + + let group = DispatchGroup() + var favicons = [Favicon]() + + faviconLinks.forEach { faviconLink in + guard let faviconUrl = URL(string: faviconLink.href) else { + return + } + + group.enter() + URLSession.default.dataTask(with: faviconUrl) { data, _, error in + guard let data = data, error == nil else { + group.leave() + return + } + + let favicon = Favicon(identifier: UUID(), + url: faviconUrl, + image: NSImage(data: data), + relationString: faviconLink.rel, + documentUrl: documentUrl, + dateCreated: Date()) + DispatchQueue.main.async { + favicons.append(favicon) + group.leave() + } + }.resume() + } + + group.notify(queue: .main) { + completion(favicons) + } + } +} diff --git a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift new file mode 100644 index 0000000000..3d278d7f5a --- /dev/null +++ b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift @@ -0,0 +1,318 @@ +// +// FaviconReferenceCache.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import os.log +import BrowserServicesKit + +final class FaviconReferenceCache { + + private let storing: FaviconStoring + + // References to favicon URLs for whole domains + private var hostReferences = [String: FaviconHostReference]() + + // References to favicon URLs for special URLs + private var urlReferences = [URL: FaviconUrlReference]() + + private var cancellables = Set() + + init(faviconStoring: FaviconStoring) { + storing = faviconStoring + } + + private(set) var loaded = false + + func loadReferences(completionHandler: ((Error?) -> Void)? = nil) { + storing.loadFaviconReferences() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("References loaded successfully", log: .favicons) + completionHandler?(nil) + case .failure(let error): + os_log("Loading of references failed: %s", log: .favicons, type: .error, error.localizedDescription) + completionHandler?(error) + } + }, receiveValue: { [weak self] (hostReferences, urlReferences) in + hostReferences.forEach { reference in + self?.hostReferences[reference.host] = reference + } + urlReferences.forEach { reference in + self?.urlReferences[reference.documentUrl] = reference + } + self?.loaded = true + }) + .store(in: &self.cancellables) + } + + func insert(faviconUrls: (smallFaviconUrl: URL?, mediumFaviconUrl: URL?), documentUrl: URL) { + guard loaded else { return } + + guard let host = documentUrl.host else { + insertToUrlCache(faviconUrls: faviconUrls, documentUrl: documentUrl) + return + } + + if let cacheEntry = hostReferences[host] { + // Host references already cached + + if cacheEntry.smallFaviconUrl == faviconUrls.smallFaviconUrl && cacheEntry.mediumFaviconUrl == faviconUrls.mediumFaviconUrl { + // Equal + return + } + + if cacheEntry.documentUrl == documentUrl { + // Favicon was updated + + // Exceptions may contain updated favicon if user visited a different documentUrl sooner + invalidateUrlCache(for: host) + insertToHostCache(faviconUrls: (faviconUrls.smallFaviconUrl, faviconUrls.mediumFaviconUrl), host: host, documentUrl: documentUrl) + return + } else { + // Exception + insertToUrlCache(faviconUrls: (faviconUrls.smallFaviconUrl, faviconUrls.mediumFaviconUrl), documentUrl: documentUrl) + + return + } + } else { + // Not cached. Add to cache + insertToHostCache(faviconUrls: (faviconUrls.smallFaviconUrl, faviconUrls.mediumFaviconUrl), host: host, documentUrl: documentUrl) + + return + } + } + + func getFaviconUrl(for documentURL: URL, sizeCategory: Favicon.SizeCategory) -> URL? { + guard loaded else { + return nil + } + + if let urlCacheEntry = urlReferences[documentURL] { + switch sizeCategory { + case .small: return urlCacheEntry.smallFaviconUrl ?? urlCacheEntry.mediumFaviconUrl + default: return urlCacheEntry.mediumFaviconUrl + } + } else if let host = documentURL.host, + let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? + hostReferences[host.dropWWW()] : hostReferences["www.\(host)"]) { + switch sizeCategory { + case .small: return hostCacheEntry.smallFaviconUrl ?? hostCacheEntry.mediumFaviconUrl + default: return hostCacheEntry.mediumFaviconUrl + } + } + + return nil + } + + func getFaviconUrl(for host: String, sizeCategory: Favicon.SizeCategory) -> URL? { + guard loaded else { + return nil + } + + let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? hostReferences[host.dropWWW()] : hostReferences["www.\(host)"]) + + switch sizeCategory { + case .small: return hostCacheEntry?.smallFaviconUrl ?? hostCacheEntry?.mediumFaviconUrl + default: return hostCacheEntry?.mediumFaviconUrl + } + } + + // MARK: - Clean + + func cleanOldExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: (() -> Void)? = nil) { + // Remove host references + removeHostReferences(filter: { hostReference in + let host = hostReference.host + return hostReference.dateCreated < Date.monthAgo && + !fireproofDomains.isFireproof(fireproofDomain: host) && + !bookmarkManager.isHostInBookmarks(host: host) + }) { + // Remove URL references + self.removeUrlReferences(filter: { urlReference in + guard let host = urlReference.documentUrl.host else { + return false + } + return urlReference.dateCreated < Date.monthAgo && + !fireproofDomains.isFireproof(fireproofDomain: host) && + !bookmarkManager.isHostInBookmarks(host: host) + }, completionHandler: completion) + } + } + + // MARK: - Burning + + func burnExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + + func isHostApproved(host: String) -> Bool { + return fireproofDomains.isFireproof(fireproofDomain: host) || + bookmarkManager.isHostInBookmarks(host: host) + } + + // Remove host references + removeHostReferences(filter: { hostReference in + let host = hostReference.host + return !isHostApproved(host: host) + }) { + // Remove URL references + self.removeUrlReferences(filter: { urlReference in + guard let host = urlReference.documentUrl.host else { + return false + } + return !isHostApproved(host: host) + }, completionHandler: completion) + } + } + + func burnDomains(_ domains: Set, + except bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + // Remove host references + removeHostReferences(filter: { hostReference in + let host = hostReference.host + return domains.contains(host) && !bookmarkManager.isHostInBookmarks(host: host) + }) { + // Remove URL references + self.removeUrlReferences(filter: { urlReference in + guard let host = urlReference.documentUrl.host else { + return false + } + return domains.contains(host) && !bookmarkManager.isHostInBookmarks(host: host) + }, completionHandler: completion) + } + } + + // MARK: - Private + + private func insertToHostCache(faviconUrls: (smallFaviconUrl: URL?, mediumFaviconUrl: URL?), host: String, documentUrl: URL) { + // Remove existing + if let oldReference = hostReferences[host] { + removeHostReferencesFromStore([oldReference]) + } + + // Create and save new references + let hostReference = FaviconHostReference(identifier: UUID(), + smallFaviconUrl: faviconUrls.smallFaviconUrl, + mediumFaviconUrl: faviconUrls.mediumFaviconUrl, + host: host, + documentUrl: documentUrl, + dateCreated: Date()) + hostReferences[host] = hostReference + + storing.save(hostReference: hostReference) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("Host reference saved successfully. host: %s", log: .favicons, hostReference.host) + case .failure(let error): + os_log("Saving of host reference failed: %s", log: .favicons, type: .error, error.localizedDescription) + } + }, receiveValue: {}) + .store(in: &self.cancellables) + } + + private func insertToUrlCache(faviconUrls: (smallFaviconUrl: URL?, mediumFaviconUrl: URL?), documentUrl: URL) { + // Remove existing + if let oldReference = urlReferences[documentUrl] { + removeUrlReferencesFromStore([oldReference]) + } + + // Create and save new references + let urlReference = FaviconUrlReference(identifier: UUID(), + smallFaviconUrl: faviconUrls.smallFaviconUrl, + mediumFaviconUrl: faviconUrls.mediumFaviconUrl, + documentUrl: documentUrl, + dateCreated: Date()) + + urlReferences[documentUrl] = urlReference + + storing.save(urlReference: urlReference) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("URL reference saved successfully. document URL: %s", log: .favicons, urlReference.documentUrl.absoluteString) + case .failure(let error): + os_log("Saving of URL reference failed: %s", log: .favicons, type: .error, error.localizedDescription) + } + }, receiveValue: {}) + .store(in: &self.cancellables) + } + + private func invalidateUrlCache(for host: String) { + removeUrlReferences { urlReference in + urlReference.documentUrl.host == host + } + } + + private func removeHostReferences(filter isRemoved: (FaviconHostReference) -> Bool, completionHandler: (() -> Void)? = nil) { + let hostReferencesToRemove = hostReferences.values.filter(isRemoved) + hostReferencesToRemove.forEach { hostReferences[$0.host] = nil } + + removeHostReferencesFromStore(hostReferencesToRemove, completionHandler: completionHandler) + } + + private func removeHostReferencesFromStore(_ hostReferences: [FaviconHostReference], completionHandler: (() -> Void)? = nil) { + guard !hostReferences.isEmpty else { completionHandler?(); return } + + storing.remove(hostReferences: hostReferences) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("Host references removed successfully.", log: .favicons) + case .failure(let error): + os_log("Removing of host references failed: %s", log: .favicons, type: .error, error.localizedDescription) + } + completionHandler?() + }, receiveValue: {}) + .store(in: &self.cancellables) + } + + private func removeUrlReferences(filter isRemoved: (FaviconUrlReference) -> Bool, completionHandler: (() -> Void)? = nil) { + let urlReferencesToRemove = urlReferences.values.filter(isRemoved) + urlReferencesToRemove.forEach { urlReferences[$0.documentUrl] = nil } + + removeUrlReferencesFromStore(urlReferencesToRemove, completionHandler: completionHandler) + } + + private func removeUrlReferencesFromStore(_ urlReferences: [FaviconUrlReference], completionHandler: (() -> Void)? = nil) { + guard !urlReferences.isEmpty else { completionHandler?(); return } + + self.storing.remove(urlReferences: urlReferences) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + os_log("URL references removed successfully.", log: .favicons) + case .failure(let error): + os_log("Removing of URL references failed: %s", log: .favicons, type: .error, error.localizedDescription) + } + completionHandler?() + }, receiveValue: {}) + .store(in: &self.cancellables) + } + +} diff --git a/DuckDuckGo/Favicons/Model/FaviconSelector.swift b/DuckDuckGo/Favicons/Model/FaviconSelector.swift new file mode 100644 index 0000000000..928d880355 --- /dev/null +++ b/DuckDuckGo/Favicons/Model/FaviconSelector.swift @@ -0,0 +1,61 @@ +// +// FaviconSelector.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +final class FaviconSelector { + + static func getMostSuitableFavicon(for sizeCategory: Favicon.SizeCategory, favicons: [Favicon]) -> Favicon? { + // Create groups according to the relation. // Prioritise favicon, then icon, and others + let faviconGroups = favicons + // Categorize into 4 categories according to the quality + .reduce(into: [[Favicon](), [Favicon](), [Favicon](), [Favicon](), [Favicon]()], { partialResult, favicon in + if favicon.sizeCategory == sizeCategory { + switch favicon.relation { + case .favicon: partialResult[0].append(favicon) + case .icon: partialResult[1].append(favicon) + case .other: partialResult[2].append(favicon) + } + } else { + // Use tiny even for small if small not available + if sizeCategory == .small && favicon.sizeCategory == .tiny { + partialResult[3].append(favicon) + } + + // Use large even for medium if medium not available + if sizeCategory == .medium && favicon.sizeCategory == .large { + partialResult[3].append(favicon) + } + + // Use small for medium if medium not available + if sizeCategory == .medium && favicon.sizeCategory == .small { + partialResult[4].insert(favicon, at: 0) + } + } + }) + + // Pick the most suitable + for faviconGroup in faviconGroups { + if let favicon = faviconGroup.first { + return favicon + } + } + return nil + } + +} diff --git a/Unit Tests/BrowserTab/Services/FaviconServiceMock.swift b/DuckDuckGo/Favicons/Model/FaviconSize.swift similarity index 51% rename from Unit Tests/BrowserTab/Services/FaviconServiceMock.swift rename to DuckDuckGo/Favicons/Model/FaviconSize.swift index 651ce89f06..fe7b676fba 100644 --- a/Unit Tests/BrowserTab/Services/FaviconServiceMock.swift +++ b/DuckDuckGo/Favicons/Model/FaviconSize.swift @@ -1,5 +1,5 @@ // -// FaviconServiceMock.swift +// FaviconSize.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -16,22 +16,14 @@ // limitations under the License. // -import XCTest -import Combine -@testable import DuckDuckGo_Privacy_Browser +import Foundation -final class FaviconServiceMock: FaviconService { +enum FaviconSize: Int { - var cachedFaviconsPublisher = PassthroughSubject<(host: String, favicon: NSImage), Never>() + // 16x16 points or bigger + case small = 16 - func fetchFavicon(_ faviconUrl: URL?, for host: String, isFromUserScript: Bool, completion: @escaping (NSImage?, Error?) -> Void) { - } - - func getCachedFavicon(for host: String, mustBeFromUserScript: Bool) -> NSImage? { - return nil - } - - func cacheIfNeeded(favicon: NSImage, for host: String, isFromUserScript: Bool) { - } + // 66x66 points or bigger + case medium = 66 } diff --git a/DuckDuckGo/Favicons/Model/FaviconUrlReference.swift b/DuckDuckGo/Favicons/Model/FaviconUrlReference.swift new file mode 100644 index 0000000000..f5b477fa76 --- /dev/null +++ b/DuckDuckGo/Favicons/Model/FaviconUrlReference.swift @@ -0,0 +1,29 @@ +// +// FaviconUrlReference.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct FaviconUrlReference { + + let identifier: UUID + let smallFaviconUrl: URL? + let mediumFaviconUrl: URL? + let documentUrl: URL + let dateCreated: Date + +} diff --git a/DuckDuckGo/BrowserTab/Model/FaviconUserScript.swift b/DuckDuckGo/Favicons/Model/FaviconUserScript.swift similarity index 60% rename from DuckDuckGo/BrowserTab/Model/FaviconUserScript.swift rename to DuckDuckGo/Favicons/Model/FaviconUserScript.swift index ea9dfd4607..38a26b8674 100644 --- a/DuckDuckGo/BrowserTab/Model/FaviconUserScript.swift +++ b/DuckDuckGo/Favicons/Model/FaviconUserScript.swift @@ -22,12 +22,19 @@ import BrowserServicesKit protocol FaviconUserScriptDelegate: AnyObject { - func faviconUserScript(_ faviconUserScript: FaviconUserScript, didFindFavicon faviconUrl: URL) + func faviconUserScript(_ faviconUserScript: FaviconUserScript, + didFindFaviconLinks faviconLinks: [FaviconUserScript.FaviconLink], + for documentUrl: URL) } final class FaviconUserScript: NSObject, StaticUserScript { + struct FaviconLink { + let href: String + let rel: String + } + static var injectionTime: WKUserScriptInjectionTime { .atDocumentEnd } static var forMainFrameOnly: Bool { true } static var script: WKUserScript = FaviconUserScript.makeWKUserScript() @@ -36,9 +43,29 @@ final class FaviconUserScript: NSObject, StaticUserScript { weak var delegate: FaviconUserScriptDelegate? func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if let urlString = message.body as? String, let url = URL(string: urlString) { - delegate?.faviconUserScript(self, didFindFavicon: url) + guard let body = message.body as? [String: Any], + let favicons = body["favicons"] as? [[String: Any]], + let documentUrlString = body["documentUrl"] as? String else { + assertionFailure("FaviconUserScript: Bad message body") + return + } + + let faviconLinks = favicons.compactMap { favicon -> FaviconLink? in + if let href = favicon["href"] as? String, + let rel = favicon["rel"] as? String { + return FaviconLink(href: href, rel: rel) + } else { + assertionFailure("FaviconUserScript: Failed to get favicon link data") + return nil + } } + + guard let documentUrl = URL(string: documentUrlString) else { + assertionFailure("FaviconUserScript: Failed to make URL from string") + return + } + + delegate?.faviconUserScript(self, didFindFaviconLinks: faviconLinks, for: documentUrl) } static let source = """ @@ -49,6 +76,7 @@ final class FaviconUserScript: NSObject, StaticUserScript { function findFavicons() { var selectors = [ + "link[rel='favicon']", "link[rel~='icon']", "link[rel='apple-touch-icon']", "link[rel='apple-touch-icon-precomposed']" @@ -59,19 +87,20 @@ final class FaviconUserScript: NSObject, StaticUserScript { var icons = document.head.querySelectorAll(selector); for (var i = 0; i < icons.length; i++) { var href = icons[i].href; + var rel = icons[i].rel; // Exclude SVGs since we can't handle them if (href.indexOf("svg") >= 0 || (icons[i].type && icons[i].type.indexOf("svg") >= 0)) { continue; } - favicons.push(href) + favicons.push({ href: href, rel: rel }); } } return favicons; }; try { - var favicon = getFavicon(); - webkit.messageHandlers.faviconFound.postMessage(favicon); + var favicons = findFavicons(); + webkit.messageHandlers.faviconFound.postMessage({ favicons: favicons, documentUrl: document.URL }); } catch(error) { // webkit might not be defined } diff --git a/DuckDuckGo/Favicons/Services/FaviconStore.swift b/DuckDuckGo/Favicons/Services/FaviconStore.swift new file mode 100644 index 0000000000..8fab9115ca --- /dev/null +++ b/DuckDuckGo/Favicons/Services/FaviconStore.swift @@ -0,0 +1,348 @@ +// +// FaviconStore.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Cocoa +import CoreData +import Combine +import os.log + +protocol FaviconStoring { + + func loadFavicons() -> Future<[Favicon], Error> + func save(favicon: Favicon) -> Future + func removeFavicons(_ favicons: [Favicon]) -> Future + + func loadFaviconReferences() -> Future<([FaviconHostReference], [FaviconUrlReference]), Error> + func save(hostReference: FaviconHostReference) -> Future + func save(urlReference: FaviconUrlReference) -> Future + func remove(hostReferences: [FaviconHostReference]) -> Future + func remove(urlReferences: [FaviconUrlReference]) -> Future + +} + +final class FaviconStore: FaviconStoring { + + enum FaviconStoreError: Error { + case notLoadedYet + case storeDeallocated + case savingFailed + } + + private lazy var context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "Favicons") + + init() {} + + init(context: NSManagedObjectContext) { + self.context = context + } + + func loadFavicons() -> Future<[Favicon], Error> { + return Future { [weak self] promise in + self?.context.perform { + guard let self = self else { + promise(.failure(FaviconStoreError.storeDeallocated)) + return + } + + let fetchRequest = FaviconManagedObject.fetchRequest() as NSFetchRequest + fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(FaviconManagedObject.dateCreated), ascending: true)] + fetchRequest.returnsObjectsAsFaults = false + do { + let faviconMOs = try self.context.fetch(fetchRequest) + os_log("%d favicons loaded ", log: .favicons, faviconMOs.count) + let favicons = faviconMOs.compactMap { Favicon(faviconMO: $0) } + promise(.success(favicons)) + } catch { + promise(.failure(error)) + } + } + } + } + + func removeFavicons(_ favicons: [Favicon]) -> Future { + let identifiers = favicons.map { $0.identifier } + return remove(identifiers: identifiers, entityName: FaviconManagedObject.className()) + } + + func save(favicon: Favicon) -> Future { + return Future { [weak self] promise in + self?.context.perform { [weak self] in + guard let self = self else { + promise(.failure(FaviconStoreError.storeDeallocated)) + return + } + + let insertedObject = NSEntityDescription.insertNewObject(forEntityName: FaviconManagedObject.className(), into: self.context) + guard let faviconMO = insertedObject as? FaviconManagedObject else { + promise(.failure(FaviconStoreError.savingFailed)) + return + } + faviconMO.update(favicon: favicon) + + do { + try self.context.save() + } catch { + promise(.failure(FaviconStoreError.savingFailed)) + return + } + + promise(.success(())) + } + } + } + + func loadFaviconReferences() -> Future<([FaviconHostReference], [FaviconUrlReference]), Error> { + return Future { [weak self] promise in + self?.context.perform { + guard let self = self else { + promise(.failure(FaviconStoreError.storeDeallocated)) + return + } + + let hostFetchRequest = FaviconHostReferenceManagedObject.fetchRequest() as NSFetchRequest + hostFetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(FaviconHostReferenceManagedObject.dateCreated), ascending: true)] + hostFetchRequest.returnsObjectsAsFaults = false + let faviconHostReferences: [FaviconHostReference] + do { + let faviconHostReferenceMOs = try self.context.fetch(hostFetchRequest) + os_log("%d favicon host references loaded ", log: .favicons, faviconHostReferenceMOs.count) + faviconHostReferences = faviconHostReferenceMOs.compactMap { FaviconHostReference(faviconHostReferenceMO: $0) } + } catch { + promise(.failure(error)) + return + } + + let urlFetchRequest = FaviconUrlReferenceManagedObject.fetchRequest() as NSFetchRequest + urlFetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(FaviconUrlReferenceManagedObject.dateCreated), ascending: true)] + urlFetchRequest.returnsObjectsAsFaults = false + do { + let faviconUrlReferenceMOs = try self.context.fetch(urlFetchRequest) + os_log("%d favicon url references loaded ", log: .favicons, faviconUrlReferenceMOs.count) + let faviconUrlReferences = faviconUrlReferenceMOs.compactMap { FaviconUrlReference(faviconUrlReferenceMO: $0) } + promise(.success((faviconHostReferences, faviconUrlReferences))) + } catch { + promise(.failure(error)) + } + } + } + } + + func save(hostReference: FaviconHostReference) -> Future { + return Future { [weak self] promise in + self?.context.perform { [weak self] in + guard let self = self else { + promise(.failure(FaviconStoreError.storeDeallocated)) + return + } + + let insertedObject = NSEntityDescription.insertNewObject(forEntityName: FaviconHostReferenceManagedObject.className(), + into: self.context) + guard let faviconHostReferenceMO = insertedObject as? FaviconHostReferenceManagedObject else { + promise(.failure(FaviconStoreError.savingFailed)) + return + } + faviconHostReferenceMO.update(hostReference: hostReference) + + do { + try self.context.save() + } catch { + promise(.failure(FaviconStoreError.savingFailed)) + return + } + + promise(.success(())) + } + } + } + + func save(urlReference: FaviconUrlReference) -> Future { + return Future { [weak self] promise in + self?.context.perform { [weak self] in + guard let self = self else { + promise(.failure(FaviconStoreError.storeDeallocated)) + return + } + + let insertedObject = NSEntityDescription.insertNewObject(forEntityName: FaviconUrlReferenceManagedObject.className(), + into: self.context) + guard let faviconUrlReferenceMO = insertedObject as? FaviconUrlReferenceManagedObject else { + promise(.failure(FaviconStoreError.savingFailed)) + return + } + faviconUrlReferenceMO.update(urlReference: urlReference) + + do { + try self.context.save() + } catch { + promise(.failure(FaviconStoreError.savingFailed)) + return + } + + promise(.success(())) + } + } + } + + func remove(hostReferences: [FaviconHostReference]) -> Future { + let identifiers = hostReferences.map { $0.identifier } + return remove(identifiers: identifiers, entityName: FaviconHostReferenceManagedObject.className()) + } + + func remove(urlReferences: [FaviconUrlReference]) -> Future { + let identifiers = urlReferences.map { $0.identifier } + return remove(identifiers: identifiers, entityName: FaviconUrlReferenceManagedObject.className()) + } + + // MARK: - Private + + private func remove(identifiers: [UUID], entityName: String) -> Future { + return Future { [weak self] promise in + self?.context.perform { + guard let self = self else { + promise(.failure(FaviconStoreError.storeDeallocated)) + return + } + + // To avoid long predicate, execute multiple times + let chunkedIdentifiers = identifiers.chunked(into: 100) + + for identifiers in chunkedIdentifiers { + let deleteRequest = NSFetchRequest(entityName: entityName) + let predicates = identifiers.map({ NSPredicate(format: "identifier == %@", argumentArray: [$0]) }) + deleteRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: predicates) + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: deleteRequest) + batchDeleteRequest.resultType = .resultTypeObjectIDs + do { + let result = try self.context.execute(batchDeleteRequest) as? NSBatchDeleteResult + let deletedObjects = result?.result as? [NSManagedObjectID] ?? [] + let changes: [AnyHashable: Any] = [ NSDeletedObjectsKey: deletedObjects ] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self.context]) + os_log("%d entries of %s removed", log: .history, entityName, deletedObjects.count) + } catch { + promise(.failure(error)) + } + } + promise(.success(())) + } + } + } + +} + +fileprivate extension Favicon { + + init?(faviconMO: FaviconManagedObject) { + guard let identifier = faviconMO.identifier, + let url = faviconMO.urlEncrypted as? URL, + let documentUrl = faviconMO.documentUrlEncrypted as? URL, + let dateCreated = faviconMO.dateCreated, + let relation = Favicon.Relation(rawValue: Int(faviconMO.relation)) else { + assertionFailure("Favicon: Failed to init Favicon from FaviconManagedObject") + return nil + } + + let image = faviconMO.imageEncrypted as? NSImage + + self.init(identifier: identifier, url: url, image: image, relation: relation, documentUrl: documentUrl, dateCreated: dateCreated) + } + +} + +fileprivate extension FaviconHostReference { + + init?(faviconHostReferenceMO: FaviconHostReferenceManagedObject) { + guard let identifier = faviconHostReferenceMO.identifier, + let host = faviconHostReferenceMO.hostEncrypted as? String, + let documentUrl = faviconHostReferenceMO.documentUrlEncrypted as? URL, + let dateCreated = faviconHostReferenceMO.dateCreated else { + assertionFailure("Favicon: Failed to init FaviconHostReference from FaviconHostReferenceManagedObject") + return nil + } + + let smallFaviconUrl = faviconHostReferenceMO.smallFaviconUrlEncrypted as? URL + let mediumFaviconUrl = faviconHostReferenceMO.mediumFaviconUrlEncrypted as? URL + + self.init(identifier: identifier, + smallFaviconUrl: smallFaviconUrl, + mediumFaviconUrl: mediumFaviconUrl, + host: host, + documentUrl: documentUrl, + dateCreated: dateCreated) + } + +} + +fileprivate extension FaviconUrlReference { + + init?(faviconUrlReferenceMO: FaviconUrlReferenceManagedObject) { + guard let identifier = faviconUrlReferenceMO.identifier, + let documentUrl = faviconUrlReferenceMO.documentUrlEncrypted as? URL, + let dateCreated = faviconUrlReferenceMO.dateCreated else { + assertionFailure("Favicon: Failed to init FaviconUrlReference from FaviconUrlReferenceManagedObject") + return nil + } + + let smallFaviconUrl = faviconUrlReferenceMO.smallFaviconUrlEncrypted as? URL + let mediumFaviconUrl = faviconUrlReferenceMO.mediumFaviconUrlEncrypted as? URL + + self.init(identifier: identifier, + smallFaviconUrl: smallFaviconUrl, + mediumFaviconUrl: mediumFaviconUrl, + documentUrl: documentUrl, + dateCreated: dateCreated) + } + +} + +fileprivate extension FaviconManagedObject { + + func update(favicon: Favicon) { + identifier = favicon.identifier + imageEncrypted = favicon.image + relation = Int64(favicon.relation.rawValue) + urlEncrypted = favicon.url as NSURL + documentUrlEncrypted = favicon.documentUrl as NSURL + dateCreated = favicon.dateCreated + } + +} + +fileprivate extension FaviconHostReferenceManagedObject { + + func update(hostReference: FaviconHostReference) { + identifier = hostReference.identifier + smallFaviconUrlEncrypted = hostReference.smallFaviconUrl as NSURL? + mediumFaviconUrlEncrypted = hostReference.mediumFaviconUrl as NSURL? + documentUrlEncrypted = hostReference.documentUrl as NSURL + hostEncrypted = hostReference.host as NSString + dateCreated = hostReference.dateCreated + } + +} + +fileprivate extension FaviconUrlReferenceManagedObject { + + func update(urlReference: FaviconUrlReference) { + identifier = urlReference.identifier + smallFaviconUrlEncrypted = urlReference.smallFaviconUrl as NSURL? + mediumFaviconUrlEncrypted = urlReference.mediumFaviconUrl as NSURL? + documentUrlEncrypted = urlReference.documentUrl as NSURL + dateCreated = urlReference.dateCreated + } + +} diff --git a/DuckDuckGo/Favicons/Services/Favicons.xcdatamodeld/Favicons.xcdatamodel/contents b/DuckDuckGo/Favicons/Services/Favicons.xcdatamodeld/Favicons.xcdatamodel/contents new file mode 100644 index 0000000000..74d926d4fd --- /dev/null +++ b/DuckDuckGo/Favicons/Services/Favicons.xcdatamodeld/Favicons.xcdatamodel/contents @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index e5bcaedca2..3a25ab9488 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -18,6 +18,7 @@ import Foundation import os.log +import BrowserServicesKit final class Fire { @@ -26,6 +27,7 @@ final class Fire { let permissionManager: PermissionManagerProtocol let downloadListCoordinator: DownloadListCoordinator let windowControllerManager: WindowControllersManager + let faviconManagement: FaviconManagement @Published private(set) var isBurning = false @@ -33,12 +35,14 @@ final class Fire { historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, permissionManager: PermissionManagerProtocol = PermissionManager.shared, downloadListCoordinator: DownloadListCoordinator = DownloadListCoordinator.shared, - windowControllerManager: WindowControllersManager = WindowControllersManager.shared) { + windowControllerManager: WindowControllersManager = WindowControllersManager.shared, + faviconManagement: FaviconManagement = FaviconManager.shared) { self.webCacheManager = cacheManager self.historyCoordinating = historyCoordinating self.permissionManager = permissionManager self.downloadListCoordinator = downloadListCoordinator self.windowControllerManager = windowControllerManager + self.faviconManagement = faviconManagement } func burnDomains(_ domains: Set, completion: (() -> Void)? = nil) { @@ -65,8 +69,10 @@ final class Fire { group.enter() burnHistory(of: burningDomains, completion: { self.burnPermissions(of: burningDomains, completion: { - self.burnDownloads(of: burningDomains) - group.leave() + self.burnFavicons(for: burningDomains) { + self.burnDownloads(of: burningDomains) + group.leave() + } }) }) @@ -98,8 +104,10 @@ final class Fire { group.enter() burnHistory { self.burnPermissions { - self.burnDownloads() - group.leave() + self.burnFavicons { + self.burnDownloads() + group.leave() + } } } @@ -160,6 +168,20 @@ final class Fire { self.downloadListCoordinator.cleanupInactiveDownloads(for: domains) } + // MARK: - Favicons + + private func burnFavicons(completion: @escaping () -> Void) { + self.faviconManagement.burnExcept(fireproofDomains: FireproofDomains.shared, + bookmarkManager: LocalBookmarkManager.shared, + completion: completion) + } + + private func burnFavicons(for domains: Set, completion: @escaping () -> Void) { + self.faviconManagement.burnDomains(domains, + except: LocalBookmarkManager.shared, + completion: completion) + } + // MARK: - Windows & Tabs private func burnWindows(exceptOwnerOf tabCollectionViewModel: TabCollectionViewModel, completion: @escaping () -> Void) { diff --git a/DuckDuckGo/Fire/View/FirePopoverViewController.swift b/DuckDuckGo/Fire/View/FirePopoverViewController.swift index a7f63a9009..db81612650 100644 --- a/DuckDuckGo/Fire/View/FirePopoverViewController.swift +++ b/DuckDuckGo/Fire/View/FirePopoverViewController.swift @@ -66,7 +66,7 @@ final class FirePopoverViewController: NSViewController { tabCollectionViewModel: TabCollectionViewModel, historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, fireproofDomains: FireproofDomains = FireproofDomains.shared, - faviconService: FaviconService = LocalFaviconService.shared) { + faviconManagement: FaviconManagement = FaviconManager.shared) { self.fireViewModel = fireViewModel self.tabCollectionViewModel = tabCollectionViewModel self.historyCoordinating = historyCoordinating @@ -74,7 +74,7 @@ final class FirePopoverViewController: NSViewController { tabCollectionViewModel: tabCollectionViewModel, historyCoordinating: historyCoordinating, fireproofDomains: fireproofDomains, - faviconService: faviconService) + faviconManagement: faviconManagement) super.init(coder: coder) } diff --git a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift index bc33020499..368f0ead32 100644 --- a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift +++ b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import Cocoa import BrowserServicesKit final class FirePopoverViewModel { @@ -46,13 +46,13 @@ final class FirePopoverViewModel { tabCollectionViewModel: TabCollectionViewModel, historyCoordinating: HistoryCoordinating, fireproofDomains: FireproofDomains, - faviconService: FaviconService, + faviconManagement: FaviconManagement, initialClearingOption: ClearingOption = .allData) { self.fireViewModel = fireViewModel self.tabCollectionViewModel = tabCollectionViewModel self.historyCoordinating = historyCoordinating self.fireproofDomains = fireproofDomains - self.faviconService = faviconService + self.faviconManagement = faviconManagement self.clearingOption = initialClearingOption updateItems(for: initialClearingOption) } @@ -67,7 +67,7 @@ final class FirePopoverViewModel { private let tabCollectionViewModel: TabCollectionViewModel private let historyCoordinating: HistoryCoordinating private let fireproofDomains: FireproofDomains - private let faviconService: FaviconService + private let faviconManagement: FaviconManagement @Published private(set) var fireproofed: [Item] = [] @Published private(set) var selectable: [Item] = [] @@ -115,10 +115,10 @@ final class FirePopoverViewModel { .subtracting(fireproofed) self.fireproofed = fireproofed - .map { Item(domain: $0, favicon: faviconService.getCachedFavicon(for: $0, mustBeFromUserScript: false)) } + .map { Item(domain: $0, favicon: faviconManagement.getCachedFavicon(for: $0, sizeCategory: .small)?.image) } .sorted { $0.domain < $1.domain } self.selectable = selectable - .map { Item(domain: $0, favicon: faviconService.getCachedFavicon(for: $0, mustBeFromUserScript: false)) } + .map { Item(domain: $0, favicon: faviconManagement.getCachedFavicon(for: $0, sizeCategory: .small)?.image) } .sorted { $0.domain < $1.domain } selectAll() } diff --git a/DuckDuckGo/History/Services/History.xcdatamodeld/History 2.xcdatamodel/contents b/DuckDuckGo/History/Services/History.xcdatamodeld/History 2.xcdatamodel/contents index 208f090766..dba5368eac 100644 --- a/DuckDuckGo/History/Services/History.xcdatamodeld/History 2.xcdatamodel/contents +++ b/DuckDuckGo/History/Services/History.xcdatamodeld/History 2.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.swift b/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.swift index a9a99e5a57..982ac960d7 100644 --- a/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.swift +++ b/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.swift @@ -61,7 +61,7 @@ final class HomepageCollectionViewItem: NSCollectionViewItem { func set(bookmarkViewModel: BookmarkViewModel) { if let bookmark = bookmarkViewModel.entity as? Bookmark { - if let favicon = bookmark.favicon { + if let favicon = bookmark.favicon(.medium) ?? bookmark.favicon(.small) { faviconImageView.image = favicon faviconImageView.layer?.backgroundColor = NSColor.clear.cgColor representingCharacterTextField.isHidden = true diff --git a/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.xib b/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.xib index e421fbbde7..08f0074881 100644 --- a/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.xib +++ b/DuckDuckGo/Homepage/View/HomepageCollectionViewItem.xib @@ -1,8 +1,8 @@ - + - + @@ -37,7 +37,7 @@ - + @@ -59,13 +59,13 @@ - + - - + + - + diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift index 2005c86aed..130cdd4e49 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift @@ -62,7 +62,7 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { } let listItemViewModel = WKBackForwardListItemViewModel(backForwardListItem: listItem, - faviconService: LocalFaviconService.shared, + faviconManagement: FaviconManager.shared, historyCoordinating: HistoryCoordinator.shared, isCurrentItem: listItems[safe: index] === currentListItem) diff --git a/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift b/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift index a2f319fb9a..9d16849c94 100644 --- a/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift +++ b/DuckDuckGo/NavigationBar/ViewModel/WKBackForwardListItemViewModel.swift @@ -22,13 +22,16 @@ import WebKit final class WKBackForwardListItemViewModel { private let backForwardListItem: BackForwardListItem - private let faviconService: FaviconService + private let faviconManagement: FaviconManagement private let historyCoordinating: HistoryCoordinating private let isCurrentItem: Bool - init(backForwardListItem: BackForwardListItem, faviconService: FaviconService, historyCoordinating: HistoryCoordinating, isCurrentItem: Bool) { + init(backForwardListItem: BackForwardListItem, + faviconManagement: FaviconManagement, + historyCoordinating: HistoryCoordinating, + isCurrentItem: Bool) { self.backForwardListItem = backForwardListItem - self.faviconService = faviconService + self.faviconManagement = faviconManagement self.historyCoordinating = historyCoordinating self.isCurrentItem = isCurrentItem } @@ -65,9 +68,10 @@ final class WKBackForwardListItemViewModel { return NSImage(named: "HomeFavicon") } - if let host = backForwardListItem.url?.host, let favicon = faviconService.getCachedFavicon(for: host, mustBeFromUserScript: false) { - favicon.size = NSSize.faviconSize - return favicon + if let url = backForwardListItem.url, + let favicon = faviconManagement.getCachedFavicon(for: url, sizeCategory: .small), + let image = favicon.image?.resizedToFaviconSize() { + return image } return NSImage(named: "DefaultFavicon") diff --git a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift index 89cd62bd0e..80d51690ab 100644 --- a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift +++ b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift @@ -34,6 +34,8 @@ final class FireproofDomainsViewController: NSViewController { @IBOutlet var tableView: NSTableView! @IBOutlet var removeDomainButton: NSButton! + private let faviconManagement: FaviconManagement = FaviconManager.shared + private var allFireproofDomains = [String]() private var filteredFireproofDomains: [String]? @@ -97,7 +99,7 @@ extension FireproofDomainsViewController: NSTableViewDataSource, NSTableViewDele if let cell = tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil) as? NSTableCellView { let domain = fireproofDomains[row] cell.textField?.stringValue = domain.dropWWW() - cell.imageView?.image = LocalFaviconService.shared.getCachedFavicon(for: domain, mustBeFromUserScript: false) + cell.imageView?.image = faviconManagement.getCachedFavicon(for: domain, sizeCategory: .small)?.image cell.imageView?.applyFaviconStyle() return cell diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index 8f18b2b56a..9e2513fabd 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -55,6 +55,8 @@ final class SaveCredentialsViewController: NSViewController { private var credentials: SecureVaultModels.WebsiteCredentials? + private var faviconManagement: FaviconManagement = FaviconManager.shared + private var saveButtonAction: (() -> Void)? var passwordData: Data { @@ -153,7 +155,7 @@ final class SaveCredentialsViewController: NSViewController { } func loadFaviconForDomain(_ domain: String) { - faviconImage.image = LocalFaviconService.shared.getCachedFavicon(for: domain, mustBeFromUserScript: false) + faviconImage.image = faviconManagement.getCachedFavicon(for: domain, sizeCategory: .small)?.image ?? NSImage(named: NSImage.Name("Web")) } diff --git a/DuckDuckGo/State Restoration/AppStateRestorationManager.swift b/DuckDuckGo/State Restoration/AppStateRestorationManager.swift index fc7b4e4856..53c2324b2f 100644 --- a/DuckDuckGo/State Restoration/AppStateRestorationManager.swift +++ b/DuckDuckGo/State Restoration/AppStateRestorationManager.swift @@ -42,8 +42,11 @@ final class AppStateRestorationManager { cancellable = WindowControllersManager.shared.stateChanged .debounce(for: .seconds(1), scheduler: RunLoop.main) - .sink { [unowned self] _ in - self.stateDidChange() + // There is a favicon assignment after a restored tab loads that triggered unnecessary + // saving of the state + .dropFirst() + .sink { [weak self] _ in + self?.stateDidChange() } } diff --git a/DuckDuckGo/Statistics/ATB/VariantManager.swift b/DuckDuckGo/Statistics/ATB/VariantManager.swift index a750afe84e..8b96171c85 100644 --- a/DuckDuckGo/Statistics/ATB/VariantManager.swift +++ b/DuckDuckGo/Statistics/ATB/VariantManager.swift @@ -107,7 +107,7 @@ final class DefaultVariantManager: VariantManager { _ = newInstallCompletion(self) return } - + storage.variant = variant.name newInstallCompletion(self) } diff --git a/Unit Tests/App/AppStateChangePublisherTests.swift b/Unit Tests/App/AppStateChangePublisherTests.swift index 69026df3ce..f45c8a0b98 100644 --- a/Unit Tests/App/AppStateChangePublisherTests.swift +++ b/Unit Tests/App/AppStateChangePublisherTests.swift @@ -221,10 +221,11 @@ final class AppStateChangePublisherTests: XCTestCase { func testWhenTabURLChangedThenStateChangePublished() { WindowsManager.openNewWindow() - let e = expectation(description: "Reordering tabs fires State changes") + var e: XCTestExpectation? = expectation(description: "Reordering tabs fires State changes") WindowControllersManager.shared.stateChanged .sink { _ in - e.fulfill() + e?.fulfill() + e = nil }.store(in: &cancellables) WindowControllersManager.shared.mainWindowControllers[0].mainViewController.tabCollectionViewModel diff --git a/Unit Tests/Bookmarks/Model/BookmarkListTests.swift b/Unit Tests/Bookmarks/Model/BookmarkListTests.swift index bc5bf55785..ee621227e3 100644 --- a/Unit Tests/Bookmarks/Model/BookmarkListTests.swift +++ b/Unit Tests/Bookmarks/Model/BookmarkListTests.swift @@ -79,7 +79,6 @@ final class BookmarkListTests: XCTestCase { let unknownBookmark = Bookmark(id: UUID(), url: URL.duckDuckGoAutocomplete, title: "Unknown title", - favicon: nil, isFavorite: true) bookmarkList.update(with: unknownBookmark) @@ -98,9 +97,9 @@ final class BookmarkListTests: XCTestCase { var bookmarkList = BookmarkList() let bookmarks = [ - Bookmark(id: UUID(), url: URL(string: "wikipedia.org")!, title: "Title", favicon: nil, isFavorite: true), - Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", favicon: nil, isFavorite: true), - Bookmark(id: UUID(), url: URL(string: "apple.com")!, title: "Title", favicon: nil, isFavorite: true) + Bookmark(id: UUID(), url: URL(string: "wikipedia.org")!, title: "Title", isFavorite: true), + Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", isFavorite: true), + Bookmark(id: UUID(), url: URL(string: "apple.com")!, title: "Title", isFavorite: true) ] bookmarks.forEach { bookmarkList.insert($0) } let bookmarkToReplace = bookmarks[2] @@ -117,8 +116,8 @@ final class BookmarkListTests: XCTestCase { let firstUrl = URL(string: "wikipedia.org")! let bookmarks = [ - Bookmark(id: UUID(), url: firstUrl, title: "Title", favicon: nil, isFavorite: true), - Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", favicon: nil, isFavorite: true) + Bookmark(id: UUID(), url: firstUrl, title: "Title", isFavorite: true), + Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", isFavorite: true) ] bookmarks.forEach { bookmarkList.insert($0) } @@ -139,7 +138,7 @@ fileprivate extension Bookmark { static var aBookmark: Bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", - favicon: nil, - isFavorite: false) + isFavorite: false, + faviconManagement: FaviconManagerMock()) } diff --git a/Unit Tests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift b/Unit Tests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift index ce631514eb..0904f6b914 100644 --- a/Unit Tests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift +++ b/Unit Tests/Bookmarks/Model/BookmarkOutlineViewDataSourceTests.swift @@ -78,8 +78,8 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { func testWhenValidatingBookmarkDrop_AndDestinationIsFolder_ThenMoveDragOperationIsReturned() { let mockDestinationFolder = BookmarkFolder.mock let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [mockDestinationFolder] bookmarkManager.loadBookmarks() @@ -98,8 +98,8 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { func testWhenValidatingFolderDrop_AndDestinationIsFolder_ThenMoveDragOperationIsReturned() { let mockDestinationFolder = BookmarkFolder.mock let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [mockDestinationFolder] bookmarkManager.loadBookmarks() @@ -118,8 +118,8 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { func testWhenValidatingFolderDrop_AndDestinationIsSameFolder_ThenNoDragOperationIsReturned() { let mockDestinationFolder = BookmarkFolder.mock let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [mockDestinationFolder] bookmarkManager.loadBookmarks() @@ -140,8 +140,8 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { let rootFolder = BookmarkFolder(id: UUID(), title: "Root", children: [childFolder]) let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [rootFolder] bookmarkManager.loadBookmarks() @@ -162,8 +162,8 @@ class BookmarkOutlineViewDataSourceTests: XCTestCase { private func createTreeController(with bookmarks: [BaseBookmarkEntity]) -> BookmarkTreeController { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = bookmarks bookmarkManager.loadBookmarks() @@ -179,7 +179,6 @@ fileprivate extension Bookmark { static var mock: Bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", - favicon: nil, isFavorite: false) } diff --git a/Unit Tests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift b/Unit Tests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift index df6292f2b6..7d28c67523 100644 --- a/Unit Tests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift +++ b/Unit Tests/Bookmarks/Model/BookmarkSidebarTreeControllerTests.swift @@ -46,8 +46,8 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { func testWhenBookmarkStoreHasNoTopLevelFolders_ThenTheDefaultBookmarksNodeHasNoChildren() { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [Bookmark.mock] bookmarkManager.loadBookmarks() @@ -64,8 +64,8 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { func testWhenBookmarkStoreHasTopLevelFolders_ThenTheDefaultBookmarksNodeHasThemAsChildren() { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) let topLevelFolder = BookmarkFolder.mock bookmarkStoreMock.bookmarks = [topLevelFolder] @@ -85,8 +85,8 @@ class BookmarkSidebarTreeControllerTests: XCTestCase { func testWhenBookmarkStoreHasNestedFolders_ThenTheTreeContainsNestedNodes() { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) let childFolder = BookmarkFolder(id: UUID(), title: "Child") let rootFolder = BookmarkFolder(id: UUID(), title: "Root", children: [childFolder]) @@ -120,7 +120,6 @@ fileprivate extension Bookmark { static var mock: Bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", - favicon: nil, isFavorite: false) } diff --git a/Unit Tests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/Unit Tests/Bookmarks/Model/LocalBookmarkManagerTests.swift index 777189588c..4701cf91eb 100644 --- a/Unit Tests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/Unit Tests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -29,8 +29,8 @@ final class LocalBookmarkManagerTests: XCTestCase { func testWhenBookmarksAreNotLoadedYet_ThenManagerIgnoresBookmarkingRequests() { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) XCTAssertNil(bookmarkManager.makeBookmark(for: URL.duckDuckGo, title: "Test", isFavorite: false)) XCTAssertNil(bookmarkManager.updateUrl(of: Bookmark.aBookmark, to: URL.duckDuckGoAutocomplete)) @@ -38,8 +38,8 @@ final class LocalBookmarkManagerTests: XCTestCase { func testWhenBookmarksAreLoaded_ThenTheManagerHoldsAllLoadedBookmarks() { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [Bookmark.aBookmark] bookmarkManager.loadBookmarks() @@ -52,8 +52,8 @@ final class LocalBookmarkManagerTests: XCTestCase { func testWhenLoadFails_ThenTheManagerHoldsBookmarksAreNil() { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = nil bookmarkStoreMock.loadError = BookmarkManagerError.somethingReallyBad @@ -158,29 +158,14 @@ final class LocalBookmarkManagerTests: XCTestCase { XCTAssert(bookmarkStoreMock.updateBookmarkCalled) } - func testWhenNewFaviconIsCached_ThenManagerUpdatesItAlsoInStore() { - let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) - - bookmarkStoreMock.bookmarks = [Bookmark.aBookmark] - bookmarkManager.loadBookmarks() - - let newFavicon = NSImage(named: "HomeFavicon")! - faviconServiceMock.cachedFaviconsPublisher.send((URL.duckDuckGo.host!, newFavicon)) - - XCTAssert(bookmarkStoreMock.updateBookmarkCalled) - XCTAssert(bookmarkManager.getBookmark(for: URL.duckDuckGo)?.favicon == newFavicon) - } - } fileprivate extension LocalBookmarkManager { static var aManager: (LocalBookmarkManager, BookmarkStoreMock) { let bookmarkStoreMock = BookmarkStoreMock() - let faviconServiceMock = FaviconServiceMock() - let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconService: faviconServiceMock) + let faviconManagerMock = FaviconManagerMock() + let bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStoreMock, faviconManagement: faviconManagerMock) bookmarkStoreMock.bookmarks = [] bookmarkManager.loadBookmarks() @@ -195,7 +180,6 @@ fileprivate extension Bookmark { static var aBookmark: Bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "Title", - favicon: nil, isFavorite: false) } diff --git a/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift index b2e1dd0256..d83e83ee48 100644 --- a/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -32,7 +32,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "DuckDuckGo", favicon: nil, isFavorite: true) + let bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "DuckDuckGo", isFavorite: true) bookmarkStore.save(bookmark: bookmark, parent: nil) { (success, error) in XCTAssert(success) @@ -63,7 +63,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let removingExpectation = self.expectation(description: "Removing") let loadingExpectation = self.expectation(description: "Loading") - let bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "DuckDuckGo", favicon: nil, isFavorite: true) + let bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "DuckDuckGo", isFavorite: true) bookmarkStore.save(bookmark: bookmark, parent: nil) { (success, error) in XCTAssert(success) XCTAssertNil(error) @@ -98,7 +98,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let savingExpectation = self.expectation(description: "Saving") let loadingExpectation = self.expectation(description: "Loading") - let bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "DuckDuckGo", favicon: nil, isFavorite: true) + let bookmark = Bookmark(id: UUID(), url: URL.duckDuckGo, title: "DuckDuckGo", isFavorite: true) bookmarkStore.save(bookmark: bookmark, parent: nil) { (success, error) in XCTAssert(success) @@ -106,7 +106,7 @@ final class LocalBookmarkStoreTests: XCTestCase { savingExpectation.fulfill() - let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo, title: "New Title", favicon: nil, isFavorite: false) + let modifiedBookmark = Bookmark(id: bookmark.id, url: URL.duckDuckGo, title: "New Title", isFavorite: false) bookmarkStore.update(bookmark: modifiedBookmark) bookmarkStore.loadAll(type: .bookmarks) { bookmarks, error in diff --git a/Unit Tests/BrowserTab/Services/FaviconManagerMock.swift b/Unit Tests/BrowserTab/Services/FaviconManagerMock.swift new file mode 100644 index 0000000000..edd76bd7a7 --- /dev/null +++ b/Unit Tests/BrowserTab/Services/FaviconManagerMock.swift @@ -0,0 +1,53 @@ +// +// FaviconManagerMock.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +import BrowserServicesKit +@testable import DuckDuckGo_Privacy_Browser + +final class FaviconManagerMock: FaviconManagement { + + func loadFavicons() {} + var areFaviconsLoaded: Bool { return true } + + func handleFaviconLinks(_ faviconLinks: [FaviconUserScript.FaviconLink], documentUrl: URL, completion: @escaping (Favicon?) -> Void) { + completion(nil) + } + + func getCachedFavicon(for documentUrl: URL, sizeCategory: Favicon.SizeCategory) -> Favicon? { + return nil + } + + func getCachedFavicon(for host: String, sizeCategory: Favicon.SizeCategory) -> Favicon? { + return nil + } + + func burnExcept(fireproofDomains: FireproofDomains, + bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + completion() + } + + func burnDomains(_ domains: Set, + except bookmarkManager: BookmarkManager, + completion: @escaping () -> Void) { + completion() + } + +} diff --git a/Unit Tests/BrowserTab/ViewModel/TabViewModelTests.swift b/Unit Tests/BrowserTab/ViewModel/TabViewModelTests.swift index 3836a60df5..b3fb5afb37 100644 --- a/Unit Tests/BrowserTab/ViewModel/TabViewModelTests.swift +++ b/Unit Tests/BrowserTab/ViewModel/TabViewModelTests.swift @@ -162,21 +162,6 @@ final class TabViewModelTests: XCTestCase { waitForExpectations(timeout: 5, handler: nil) } - func testWhenTabDownloadedFaviconThenFaviconIsNotNil() { - let tabViewModel = TabViewModel.aTabViewModel - tabViewModel.tab.url = URL(string: "http://apple.com") - - let faviconExpectation = expectation(description: "Favicon") - - tabViewModel.$favicon.debounce(for: 1, scheduler: RunLoop.main).sink { favicon in - guard favicon != nil else { return } - - XCTAssertNotEqual(favicon, TabViewModel.Favicon.home) - faviconExpectation.fulfill() - } .store(in: &cancellables) - waitForExpectations(timeout: 5, handler: nil) - } - } extension TabViewModel { diff --git a/Unit Tests/Fire/Model/FireTests.swift b/Unit Tests/Fire/Model/FireTests.swift index 3af4aa98e7..0a27f451ae 100644 --- a/Unit Tests/Fire/Model/FireTests.swift +++ b/Unit Tests/Fire/Model/FireTests.swift @@ -30,10 +30,12 @@ final class FireTests: XCTestCase { let manager = WebCacheManagerMock() let historyCoordinator = HistoryCoordinatingMock() let permissionManager = PermissionManagerMock() + let faviconManager = FaviconManagerMock() let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, - permissionManager: permissionManager) + permissionManager: permissionManager, + faviconManagement: faviconManager) let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel let burningExpectation = expectation(description: "Burning") @@ -51,10 +53,12 @@ final class FireTests: XCTestCase { let manager = WebCacheManagerMock() let historyCoordinator = HistoryCoordinatingMock() let permissionManager = PermissionManagerMock() + let faviconManager = FaviconManagerMock() let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, - permissionManager: permissionManager) + permissionManager: permissionManager, + faviconManagement: faviconManager) let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel let finishedBurningExpectation = expectation(description: "Finished burning") @@ -73,9 +77,13 @@ final class FireTests: XCTestCase { let manager = WebCacheManagerMock() let historyCoordinator = HistoryCoordinatingMock() let permissionManager = PermissionManagerMock() + let faviconManager = FaviconManagerMock() + let fire = Fire(cacheManager: manager, historyCoordinating: historyCoordinator, - permissionManager: permissionManager) + permissionManager: permissionManager, + faviconManagement: faviconManager) + let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel let isBurningExpectation = expectation(description: "Burning") diff --git a/Unit Tests/Statistics/PixelArgumentsTests.swift b/Unit Tests/Statistics/PixelArgumentsTests.swift index ac6685a99e..567b128255 100644 --- a/Unit Tests/Statistics/PixelArgumentsTests.swift +++ b/Unit Tests/Statistics/PixelArgumentsTests.swift @@ -30,7 +30,7 @@ class PixelArgumentsTests: XCTestCase { override func setUp() { bookmarkStore = BookmarkStoreMock() bookmarkStore.bookmarks = [] - bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStore, faviconService: FaviconServiceMock()) + bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStore, faviconManagement: FaviconManagerMock()) bookmarkManager.loadBookmarks() UserDefaultsWrapper.clearAll() fireproofDomains = FireproofDomains() From fc143da1db181f498b9fd2a931175914b323cd63 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 17 Jan 2022 03:16:15 -0800 Subject: [PATCH 7/9] Fix Lock Screen UI issues (#399) --- .../MacWaitlistLockScreenViewController.swift | 3 +++ DuckDuckGo/Waitlist/View/Waitlist.storyboard | 23 ++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/Waitlist/View/MacWaitlistLockScreenViewController.swift b/DuckDuckGo/Waitlist/View/MacWaitlistLockScreenViewController.swift index a1fe48caf6..0c6c5aeea3 100644 --- a/DuckDuckGo/Waitlist/View/MacWaitlistLockScreenViewController.swift +++ b/DuckDuckGo/Waitlist/View/MacWaitlistLockScreenViewController.swift @@ -55,6 +55,9 @@ final class MacWaitlistLockScreenViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() + // Force the key equivalent to equal the return key. This value wasn't being consistently applied when set in the Storyboard. + continueButton.keyEquivalent = "\r" + // The unlock screen background uses a light mode background, so those UI elements are hardcoded. inviteCodeTextField.appearance = NSAppearance(named: .aqua) networkRequestSpinner.appearance = NSAppearance(named: .aqua) diff --git a/DuckDuckGo/Waitlist/View/Waitlist.storyboard b/DuckDuckGo/Waitlist/View/Waitlist.storyboard index dca9522195..789b1f1f3b 100644 --- a/DuckDuckGo/Waitlist/View/Waitlist.storyboard +++ b/DuckDuckGo/Waitlist/View/Waitlist.storyboard @@ -1,8 +1,8 @@ - + - + @@ -24,10 +24,10 @@ - + - + @@ -35,17 +35,17 @@ - + - + - + - + @@ -57,7 +57,7 @@ - + @@ -100,7 +100,7 @@ - + @@ -160,9 +160,6 @@ We ❤️ your feedback. When you have thoughts to share, click the ••• me - -DQ - From 5ba8ecd2de68570abcac4321709f7726d20999ca Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 17 Jan 2022 18:56:26 +0700 Subject: [PATCH 8/9] Fireproofing encrypted storage (#332) * fireproofing storage in CoreData * Added tests, some fixes * rollback 11.3 avail * fix linter issue * fixing tests * fix linter issue * Drop FireproofingURLExtensions for Login Detection * rollback Drop FireproofingURLExtensions for Login Detection * async test * CoreDataStore, Unprotected Domains Store * ConcreteDataStore.. * finalize * addToAllowed -> add(domain) * Fix Fireproofing from inactive Tab Context Menu * package.resolved * Create write context for write operations * test coverage * Fix PR Review issues * fix linter issue * fix test * rollback async editing * set Unprotected Domain to exactly match --- DuckDuckGo.xcodeproj/project.pbxproj | 94 +++++- .../Common/Database/CoreDataStore.swift | 305 ++++++++++++++++++ ...ension.swift => DictionaryExtension.swift} | 12 +- .../Utilities/UserDefaultsWrapper.swift | 6 +- .../ContentBlocker/ContentBlocking.swift | 55 +--- .../Fireproofing/Model/FireproofDomains.swift | 95 ++++-- .../Permissions.xcdatamodel/contents | 9 + .../Model/FireproofDomainsContainer.swift | 93 ++++++ .../Model/FireproofDomainsStore.swift | 43 +++ .../View/FireproofDomainsViewController.swift | 2 +- .../View/SaveCredentialsViewController.swift | 4 +- .../Smarter Encryption/HTTPSUpgrade.swift | 4 +- .../TabBar/View/TabBarViewController.swift | 25 +- .../KeySetDictionary.swift | 53 +++ .../LocalUnprotectedDomains.swift | 117 +++++++ .../Permissions.xcdatamodel/contents | 9 + .../Services/WebsiteDataStoreTests.swift | 18 +- Unit Tests/Common/CoreDataTestUtilities.swift | 8 + Unit Tests/Database/CoreDataStoreTests.swift | 163 ++++++++++ .../Permissions.xcdatamodel/contents | 10 + .../FireproofDomainsStoreMock.swift | 84 +++++ .../Fireproofing/FireproofDomainsTests.swift | 90 +++++- .../Model/HistoryCoordinatorTests.swift | 4 +- .../Permissions/PermissionManagerTests.swift | 4 +- .../Statistics/PixelArgumentsTests.swift | 4 +- 25 files changed, 1191 insertions(+), 120 deletions(-) create mode 100644 DuckDuckGo/Common/Database/CoreDataStore.swift rename DuckDuckGo/Common/Extensions/{UserDefaultsExtension.swift => DictionaryExtension.swift} (69%) create mode 100644 DuckDuckGo/Fireproofing/Model/FireproofDomains.xcdatamodeld/Permissions.xcdatamodel/contents create mode 100644 DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift create mode 100644 DuckDuckGo/Fireproofing/Model/FireproofDomainsStore.swift create mode 100644 DuckDuckGo/Unprotected Domains/KeySetDictionary.swift create mode 100644 DuckDuckGo/Unprotected Domains/LocalUnprotectedDomains.swift create mode 100644 DuckDuckGo/Unprotected Domains/UnprotectedDomains.xcdatamodeld/Permissions.xcdatamodel/contents create mode 100644 Unit Tests/Database/CoreDataStoreTests.swift create mode 100644 Unit Tests/Database/TestDataModel.xcdatamodeld/Permissions.xcdatamodel/contents create mode 100644 Unit Tests/Fireproofing/FireproofDomainsStoreMock.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index aea6270cd6..ff1b1c6be5 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 14505A08256084EF00272CC6 /* UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14505A07256084EF00272CC6 /* UserAgent.swift */; }; 1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */; }; 14D9B8FB24F7E089000D4D13 /* AddressBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */; }; - 336B39E72726BAE800C417D3 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E62726BAE800C417D3 /* UserDefaultsExtension.swift */; }; 336D5B18262D8D3C0052E0C9 /* findinpage.js in Resources */ = {isa = PBXBuildFile; fileRef = 336D5AEF262D8D3C0052E0C9 /* findinpage.js */; }; 339A6B5826A044BA00E3DAE8 /* duckduckgo-privacy-dashboard in Resources */ = {isa = PBXBuildFile; fileRef = 339A6B5726A044BA00E3DAE8 /* duckduckgo-privacy-dashboard */; }; 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; @@ -452,6 +451,10 @@ AAF7D3862567CED500998667 /* WebViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF7D3852567CED500998667 /* WebViewConfiguration.swift */; }; AAFCB37F25E545D400859DD4 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */; }; AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */; }; + B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6040855274B830F00680351 /* DictionaryExtension.swift */; }; + B604085C274B8FBA00680351 /* UnprotectedDomains.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B604085A274B8CA300680351 /* UnprotectedDomains.xcdatamodeld */; }; + B6085D062743905F00A9C456 /* CoreDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6085D052743905F00A9C456 /* CoreDataStore.swift */; }; + B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6085D072743993C00A9C456 /* FireproofDomains.xcdatamodeld */; }; B6106BA026A7BE0B0013B453 /* PermissionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9F26A7BE0B0013B453 /* PermissionManagerTests.swift */; }; B6106BA426A7BEA40013B453 /* PermissionAuthorizationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA226A7BEA00013B453 /* PermissionAuthorizationState.swift */; }; B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106BA526A7BEC80013B453 /* PermissionAuthorizationQuery.swift */; }; @@ -513,6 +516,8 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67C6C462654C643006C872E /* FileManagerExtensionTests.swift */; }; B68172A9269C487D006D1092 /* PrivacyDashboardUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68172A8269C487D006D1092 /* PrivacyDashboardUserScript.swift */; }; B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68172AD269EB43F006D1092 /* GeolocationServiceTests.swift */; }; + B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */; }; + B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */; }; B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458AF25C7E76A00DC17B6 /* WindowManager+StateRestoration.swift */; }; B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458B725C7E8B200DC17B6 /* Tab+NSSecureCoding.swift */; }; B68458C025C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68458BF25C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift */; }; @@ -522,6 +527,7 @@ B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684592125C93BE000DC17B6 /* Publisher.asVoid.swift */; }; B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684592625C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift */; }; B684592F25C93FBF00DC17B6 /* AppStateRestorationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684592E25C93FBF00DC17B6 /* AppStateRestorationManager.swift */; }; + B68503A7279141CD00893A05 /* KeySetDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68503A6279141CD00893A05 /* KeySetDictionary.swift */; }; B687260426E215C9008EE860 /* ExpirationChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B687260326E215C9008EE860 /* ExpirationChecker.swift */; }; B688B4DA273E6D3B0087BEAF /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B688B4D9273E6D3B0087BEAF /* MainView.swift */; }; B688B4DF27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B688B4DE27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift */; }; @@ -597,6 +603,8 @@ B6B1E88B26D774090062C350 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E88A26D774090062C350 /* LinkButton.swift */; }; B6B3E0962654DACD0040E0A2 /* UTTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0952654DACD0040E0A2 /* UTTypeTests.swift */; }; B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */; }; + B6BBF1702744CDE1004F850E /* CoreDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF16F2744CDE1004F850E /* CoreDataStoreTests.swift */; }; + B6BBF1722744CE36004F850E /* FireproofDomainsStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */; }; B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BBF17327475B15004F850E /* PopupBlockedPopover.swift */; }; B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B22D26E61CE70031CB7F /* DownloadViewModel.swift */; }; B6C0B23026E61D630031CB7F /* DownloadListStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B22F26E61D630031CB7F /* DownloadListStore.swift */; }; @@ -608,6 +616,7 @@ B6C0B24426E9CB080031CB7F /* RunLoopExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B24326E9CB080031CB7F /* RunLoopExtension.swift */; }; B6C0B24626E9CB190031CB7F /* RunLoopExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B24526E9CB190031CB7F /* RunLoopExtensionTests.swift */; }; B6C2C9EF276081AB005B7F0A /* DeallocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */; }; + B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C2C9F42760B659005B7F0A /* TestDataModel.xcdatamodeld */; }; B6CF78DE267B099C00CD4F13 /* WKNavigationActionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF78DD267B099C00CD4F13 /* WKNavigationActionExtension.swift */; }; B6DA44022616B28300DD1EC2 /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; }; B6DA44082616B30600DD1EC2 /* PixelDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44062616B30600DD1EC2 /* PixelDataModel.xcdatamodeld */; }; @@ -625,6 +634,7 @@ B6E53888267C94A00010FEA9 /* HomepageCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E53887267C94A00010FEA9 /* HomepageCollectionViewFlowLayout.swift */; }; B6E61EE3263AC0C8004E11AB /* FileManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */; }; B6E61EE8263ACE16004E11AB /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E61EE7263ACE16004E11AB /* UTType.swift */; }; + B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; }; B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F41030264D2B23003DA42C /* ProgressExtension.swift */; }; B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; }; B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; }; @@ -667,7 +677,7 @@ 14505A07256084EF00272CC6 /* UserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgent.swift; sourceTree = ""; }; 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarCollectionView.swift; sourceTree = ""; }; 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarViewController.swift; sourceTree = ""; }; - 336B39E62726BAE800C417D3 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = ""; }; + 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalUnprotectedDomains.swift; sourceTree = ""; }; 336D5AEF262D8D3C0052E0C9 /* findinpage.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = findinpage.js; sourceTree = ""; }; 339A6B5726A044BA00E3DAE8 /* duckduckgo-privacy-dashboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "duckduckgo-privacy-dashboard"; path = "Submodules/duckduckgo-privacy-dashboard"; sourceTree = SOURCE_ROOT; }; 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; @@ -1111,6 +1121,10 @@ AAF7D3852567CED500998667 /* WebViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewConfiguration.swift; sourceTree = ""; }; AAFCB37E25E545D400859DD4 /* PublisherExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherExtension.swift; sourceTree = ""; }; AAFE068226C7082D005434CC /* WebKitVersionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProvider.swift; sourceTree = ""; }; + B6040855274B830F00680351 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = ""; }; + B604085B274B8CA400680351 /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = ""; }; + B6085D052743905F00A9C456 /* CoreDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStore.swift; sourceTree = ""; }; + B6085D082743993D00A9C456 /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = ""; }; B6106B9D26A565DA0013B453 /* BundleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtension.swift; sourceTree = ""; }; B6106B9F26A7BE0B0013B453 /* PermissionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManagerTests.swift; sourceTree = ""; }; B6106BA226A7BEA00013B453 /* PermissionAuthorizationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAuthorizationState.swift; sourceTree = ""; }; @@ -1176,6 +1190,8 @@ B67C6C462654C643006C872E /* FileManagerExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExtensionTests.swift; sourceTree = ""; }; B68172A8269C487D006D1092 /* PrivacyDashboardUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardUserScript.swift; sourceTree = ""; }; B68172AD269EB43F006D1092 /* GeolocationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeolocationServiceTests.swift; sourceTree = ""; }; + B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsContainer.swift; sourceTree = ""; }; + B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsStore.swift; sourceTree = ""; }; B68458AF25C7E76A00DC17B6 /* WindowManager+StateRestoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowManager+StateRestoration.swift"; sourceTree = ""; }; B68458B725C7E8B200DC17B6 /* Tab+NSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+NSSecureCoding.swift"; sourceTree = ""; }; B68458BF25C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModel+NSSecureCoding.swift"; sourceTree = ""; }; @@ -1185,6 +1201,7 @@ B684592125C93BE000DC17B6 /* Publisher.asVoid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.asVoid.swift; sourceTree = ""; }; B684592625C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publishers.NestedObjectChanges.swift; sourceTree = ""; }; B684592E25C93FBF00DC17B6 /* AppStateRestorationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateRestorationManager.swift; sourceTree = ""; }; + B68503A6279141CD00893A05 /* KeySetDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeySetDictionary.swift; sourceTree = ""; }; B687260326E215C9008EE860 /* ExpirationChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpirationChecker.swift; sourceTree = ""; }; B688B4D9273E6D3B0087BEAF /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; B688B4DE27420D290087BEAF /* PDFSearchTextMenuItemHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFSearchTextMenuItemHandler.swift; sourceTree = ""; }; @@ -1262,6 +1279,8 @@ B6B1E88A26D774090062C350 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; B6B3E0952654DACD0040E0A2 /* UTTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTTypeTests.swift; sourceTree = ""; }; B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSScreenExtension.swift; sourceTree = ""; }; + B6BBF16F2744CDE1004F850E /* CoreDataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStoreTests.swift; sourceTree = ""; }; + B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofDomainsStoreMock.swift; sourceTree = ""; }; B6BBF17327475B15004F850E /* PopupBlockedPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupBlockedPopover.swift; sourceTree = ""; }; B6C0B22D26E61CE70031CB7F /* DownloadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadViewModel.swift; sourceTree = ""; }; B6C0B22F26E61D630031CB7F /* DownloadListStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListStore.swift; sourceTree = ""; }; @@ -1273,6 +1292,7 @@ B6C0B24326E9CB080031CB7F /* RunLoopExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RunLoopExtension.swift; sourceTree = ""; }; B6C0B24526E9CB190031CB7F /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeallocationTests.swift; sourceTree = ""; }; + B6C2C9F52760B659005B7F0A /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = ""; }; B6CF78DD267B099C00CD4F13 /* WKNavigationActionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationActionExtension.swift; sourceTree = ""; }; B6CF78E2267B0A1900CD4F13 /* WKNavigationAction+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WKNavigationAction+Private.h"; sourceTree = ""; }; B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelDataStore.swift; sourceTree = ""; }; @@ -1392,6 +1412,9 @@ isa = PBXGroup; children = ( 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */, + B6830960274CDE99004B46BB /* FireproofDomainsContainer.swift */, + B6830962274CDEC7004B46BB /* FireproofDomainsStore.swift */, + B6085D072743993C00A9C456 /* FireproofDomains.xcdatamodeld */, ); path = Model; sourceTree = ""; @@ -1410,6 +1433,7 @@ children = ( 4B02199925E063DE00ED7DEA /* FireproofDomainsTests.swift */, 4B02199A25E063DE00ED7DEA /* FireproofingURLExtensionsTests.swift */, + B6BBF1712744CE36004F850E /* FireproofDomainsStoreMock.swift */, ); path = Fireproofing; sourceTree = ""; @@ -1584,6 +1608,7 @@ isa = PBXGroup; children = ( 4B677440255DBEEA00025BD8 /* Database.swift */, + B6085D052743905F00A9C456 /* CoreDataStore.swift */, ); path = Database; sourceTree = ""; @@ -2297,6 +2322,7 @@ AACB8E7224A4C8BC005F2218 /* Suggestions */, AA86491124D8318F001BABEE /* TabBar */, AAE8B0FD258A416F00E81239 /* Tooltip */, + B6040859274B8C5200680351 /* Unprotected Domains */, AACF6FD426BC35C200CF09F9 /* User Agent */, 4BC68A6C2759ADC20029A586 /* Waitlist */, AA6EF9AE25066F99004754E6 /* Windows */, @@ -2321,6 +2347,7 @@ 85AC3B1525D9BBFA00C7D2AA /* Configuration */, 4B82E9B725B6A04B00656FE7 /* ContentBlocker */, 4B723E0226B0003E00E14D75 /* Data Export */, + B683097A274DCFE3004B46BB /* Database */, 4B723DFE26B0003E00E14D75 /* Data Import */, 8553FF50257523630029327F /* FileDownload */, 4BA1A6CE258BF58C00F6F690 /* FileSystem */, @@ -2954,6 +2981,7 @@ 4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */, 85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */, B6A9E46F26146A250067D1B9 /* DateExtension.swift */, + B6040855274B830F00680351 /* DictionaryExtension.swift */, B63D467025BFA6C100874977 /* DispatchQueueExtensions.swift */, AA92126E25ACCB1100600CD4 /* ErrorExtension.swift */, B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */, @@ -2999,7 +3027,6 @@ AA8EDF2324923E980071C2E8 /* URLExtension.swift */, AA88D14A252A557100980B4E /* URLRequestExtension.swift */, B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, - 336B39E62726BAE800C417D3 /* UserDefaultsExtension.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, B63D466825BEB6C200874977 /* WKWebView+SessionState.swift */, @@ -3145,6 +3172,16 @@ path = View; sourceTree = ""; }; + B6040859274B8C5200680351 /* Unprotected Domains */ = { + isa = PBXGroup; + children = ( + 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */, + B68503A6279141CD00893A05 /* KeySetDictionary.swift */, + B604085A274B8CA300680351 /* UnprotectedDomains.xcdatamodeld */, + ); + path = "Unprotected Domains"; + sourceTree = ""; + }; B6106BA126A7BE430013B453 /* Permissions */ = { isa = PBXGroup; children = ( @@ -3246,6 +3283,15 @@ path = Geolocation; sourceTree = ""; }; + B683097A274DCFE3004B46BB /* Database */ = { + isa = PBXGroup; + children = ( + B6BBF16F2744CDE1004F850E /* CoreDataStoreTests.swift */, + B6C2C9F42760B659005B7F0A /* TestDataModel.xcdatamodeld */, + ); + path = Database; + sourceTree = ""; + }; B68458AE25C7E75100DC17B6 /* State Restoration */ = { isa = PBXGroup; children = ( @@ -3770,6 +3816,7 @@ 4B0511BE262CAA5A00F6079C /* DownloadPreferences.swift in Sources */, 4B0511BC262CAA5A00F6079C /* AppearancePreferences.swift in Sources */, 4B92928D26670D1700AD2C21 /* BookmarkOutlineViewCell.swift in Sources */, + B604085C274B8FBA00680351 /* UnprotectedDomains.xcdatamodeld in Sources */, 4BB88B5025B7BA2B006F6B06 /* TabInstrumentation.swift in Sources */, 4B59024326B35F7C00489384 /* BrowserImportViewController.swift in Sources */, 4B9292D72667124000AD2C21 /* NSPopUpButtonExtension.swift in Sources */, @@ -3854,6 +3901,7 @@ 4BB99D0426FE191E001E4761 /* SafariDataImporter.swift in Sources */, 4B0511CD262CAA5A00F6079C /* DefaultBrowserTableCellView.swift in Sources */, B69B503A2726A12500758A2B /* StatisticsLoader.swift in Sources */, + B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */, 858A798526A8BB5D00A75A42 /* NSTextViewExtension.swift in Sources */, B6B1E88426D5EB570062C350 /* DownloadsCellView.swift in Sources */, 4B723E0C26B0005D00E14D75 /* CSVImportSummaryViewController.swift in Sources */, @@ -3906,12 +3954,12 @@ AA88D14B252A557100980B4E /* URLRequestExtension.swift in Sources */, AA6197C6276B3168008396F0 /* FaviconHostReference.swift in Sources */, 4B8AC93B26B48ADF00879451 /* ASN1Parser.swift in Sources */, - 336B39E72726BAE800C417D3 /* UserDefaultsExtension.swift in Sources */, B66E9DD22670EB2A00E53BB5 /* _WKDownload+WebKitDownload.swift in Sources */, B6A9E4612614608B0067D1B9 /* AppVersion.swift in Sources */, 856C98DF257014BD00A22F1F /* FileDownloadManager.swift in Sources */, 85CC1D73269EF1880062F04E /* PasswordManagementItemList.swift in Sources */, 4BB99CFF26FE191E001E4761 /* BookmarkImport.swift in Sources */, + B68503A7279141CD00893A05 /* KeySetDictionary.swift in Sources */, 85480FBB25D181CB009424E3 /* ConfigurationDownloading.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, @@ -3937,10 +3985,12 @@ 85707F2A276A35FE00DC0649 /* ActionSpeech.swift in Sources */, 8585B63826D6E66C00C1416F /* ButtonStyles.swift in Sources */, 4B0511BD262CAA5A00F6079C /* PrivacySecurityPreferences.swift in Sources */, + B6830963274CDEC7004B46BB /* FireproofDomainsStore.swift in Sources */, AA9FF95F24A1FB690039E328 /* TabCollectionViewModel.swift in Sources */, AAC5E4D125D6A709007F5990 /* BookmarkManager.swift in Sources */, 4BE65476271FCD41008D1D63 /* PasswordManagementCreditCardItemView.swift in Sources */, AA5C8F59258FE21F00748EB7 /* NSTextFieldExtension.swift in Sources */, + B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */, B65536AE2685E17200085A79 /* GeolocationService.swift in Sources */, 4B02198925E05FAC00ED7DEA /* FireproofingURLExtensions.swift in Sources */, 4BA1A6A5258B07DF00F6F690 /* EncryptedValueTransformer.swift in Sources */, @@ -3950,6 +4000,7 @@ AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */, B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */, 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, + B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, B6A924D92664C72E001A28CA /* WebKitDownloadTask.swift in Sources */, 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */, 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, @@ -4028,6 +4079,7 @@ 4B723E1326B0007A00E14D75 /* CSVLoginExporter.swift in Sources */, 8562599A269CA0A600EE44BC /* NSRectExtension.swift in Sources */, 4B0511C5262CAA5A00F6079C /* PrivacySecurityPreferencesTableCellView.swift in Sources */, + B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */, 4B677431255DBEB800025BD8 /* BloomFilterWrapper.mm in Sources */, 4B0511C8262CAA5A00F6079C /* PreferencesListViewController.swift in Sources */, B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, @@ -4058,6 +4110,7 @@ B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */, 4B92929D26670D2A00AD2C21 /* BookmarkNode.swift in Sources */, B693955226F04BEB0015B914 /* LongPressButton.swift in Sources */, + B6085D062743905F00A9C456 /* CoreDataStore.swift in Sources */, B6DB3AF6278EA0130024C5C4 /* BundleExtension.swift in Sources */, 4B677438255DBEB800025BD8 /* HTTPSUpgrade.xcdatamodeld in Sources */, 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */, @@ -4180,6 +4233,7 @@ B65349AA265CF45000DCC645 /* DispatchQueueExtensionsTests.swift in Sources */, 858A798A26A9B35E00A75A42 /* PasswordManagementItemModelTests.swift in Sources */, B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */, + B6BBF1702744CDE1004F850E /* CoreDataStoreTests.swift in Sources */, 4B9292BF2667103100AD2C21 /* TreeControllerTests.swift in Sources */, B693956926F352DB0015B914 /* DownloadsWebViewMock.m in Sources */, 4B2CBF412767EEC1001DF04B /* MacWaitlistStoreTests.swift in Sources */, @@ -4226,10 +4280,12 @@ AA63745424C9BF9A00AB2AC4 /* SuggestionContainerTests.swift in Sources */, AAC9C01524CAFBCE00AD1325 /* TabTests.swift in Sources */, B69B504C2726CA2900758A2B /* MockVariantManager.swift in Sources */, + B6BBF1722744CE36004F850E /* FireproofDomainsStoreMock.swift in Sources */, 4BA1A6D9258C0CB300F6F690 /* DataEncryptionTests.swift in Sources */, 858C1BED26974E6600E6C014 /* PasswordManagerSettingsTests.swift in Sources */, B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */, B693955F26F1C17F0015B914 /* DownloadListCoordinatorTests.swift in Sources */, + B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */, B68172AE269EB43F006D1092 /* GeolocationServiceTests.swift in Sources */, B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */, 4BA1A6FE258C5C1300F6F690 /* EncryptedValueTransformerTests.swift in Sources */, @@ -5326,6 +5382,26 @@ sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; + B604085A274B8CA300680351 /* UnprotectedDomains.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + B604085B274B8CA400680351 /* Permissions.xcdatamodel */, + ); + currentVersion = B604085B274B8CA400680351 /* Permissions.xcdatamodel */; + path = UnprotectedDomains.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; + B6085D072743993C00A9C456 /* FireproofDomains.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + B6085D082743993D00A9C456 /* Permissions.xcdatamodel */, + ); + currentVersion = B6085D082743993D00A9C456 /* Permissions.xcdatamodel */; + path = FireproofDomains.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; B64C852E26943BC10048FEBE /* Permissions.xcdatamodeld */ = { isa = XCVersionGroup; children = ( @@ -5356,6 +5432,16 @@ sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; + B6C2C9F42760B659005B7F0A /* TestDataModel.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + B6C2C9F52760B659005B7F0A /* Permissions.xcdatamodel */, + ); + currentVersion = B6C2C9F52760B659005B7F0A /* Permissions.xcdatamodel */; + path = TestDataModel.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; B6DA44062616B30600DD1EC2 /* PixelDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( diff --git a/DuckDuckGo/Common/Database/CoreDataStore.swift b/DuckDuckGo/Common/Database/CoreDataStore.swift new file mode 100644 index 0000000000..05691aa7ab --- /dev/null +++ b/DuckDuckGo/Common/Database/CoreDataStore.swift @@ -0,0 +1,305 @@ +// +// CoreDataStore.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import CoreData + +protocol ValueRepresentableManagedObject: NSManagedObject { + associatedtype ValueType + + func valueRepresentation() -> ValueType? + func update(with value: ValueType) throws +} + +enum CoreDataStoreError: Error, Equatable { + case objectNotFound + case multipleObjectsFound + case invalidManagedObject +} + +extension CoreDataStore { + + func add(_ value: Value) throws -> NSManagedObjectID { + try add([value]).first?.id ?? { throw CoreDataStoreError.objectNotFound }() + } + + func remove(objectWithId id: NSManagedObjectID) { + remove(objectWithId: id, completionHandler: nil) + } + + func remove(objectsWithPredicate predicate: NSPredicate) { + remove(objectsWithPredicate: predicate, completionHandler: nil) + } + + func clear() { + clear(completionHandler: nil) + } + +} + +internal class CoreDataStore { + + private let tableName: String + private var _readContext: NSManagedObjectContext?? + + private var readContext: NSManagedObjectContext? { + if case .none = _readContext { +#if DEBUG + if AppDelegate.isRunningTests { + _readContext = .some(.none) + return .none + } +#endif + _readContext = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: tableName) + } + return _readContext! + } + + private func writeContext() -> NSManagedObjectContext? { + guard let context = readContext else { return nil } + + let newContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + newContext.persistentStoreCoordinator = context.persistentStoreCoordinator + newContext.name = context.name + + return newContext + } + + init(context: NSManagedObjectContext? = nil, tableName: String) { + if let context = context { + self._readContext = .some(context) + } + self.tableName = tableName + } + + typealias Value = ManagedObject.ValueType + typealias IDValueTuple = (id: NSManagedObjectID, value: Value) + + func load(objectsWithPredicate predicate: NSPredicate? = nil, + sortDescriptors: [NSSortDescriptor]? = nil, + into initialResult: Result, + _ accumulate: (inout Result, IDValueTuple) throws -> Void) throws -> Result { + + var result = initialResult + var coreDataError: Error? + + guard let context = readContext else { return result } + context.performAndWait { + let fetchRequest = NSFetchRequest(entityName: ManagedObject.className()) + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = sortDescriptors + fetchRequest.returnsObjectsAsFaults = false + + do { + result = try context.fetch(fetchRequest).reduce(into: result) { result, managedObject in + guard let value = managedObject.valueRepresentation() else { return } + try accumulate(&result, (managedObject.objectID, value)) + } + } catch { + coreDataError = error + } + } + + if let coreDataError = coreDataError { + throw coreDataError + } + + return result + } + + func add(_ values: S) throws -> [(value: Value, id: NSManagedObjectID)] where S.Element == Value { + guard let context = writeContext() else { return [] } + + var result: Result<[(Value, NSManagedObjectID)], Error> = .success([]) + + context.performAndWait { [context] in + let entityName = ManagedObject.className() + var added = [(Value, NSManagedObject)]() + added.reserveCapacity(values.underestimatedCount) + + do { + for value in values { + guard let managedObject = NSEntityDescription + .insertNewObject(forEntityName: entityName, into: context) as? ManagedObject + else { + result = .failure(CoreDataStoreError.invalidManagedObject) + return + } + + try managedObject.update(with: value) + added.append((value, managedObject)) + } + + try context.save() + result = .success(added.map { ($0, $1.objectID) }) + } catch { + result = .failure(error) + } + } + return try result.get() + } + + func update(objectWithPredicate predicate: NSPredicate, with value: Value, completionHandler: ((Error?) -> Void)?) { + guard let context = writeContext() else { return } + + func mainQueueCompletion(_ error: Error?) { + guard completionHandler != nil else { return } + DispatchQueue.main.async { + completionHandler?(error) + } + } + + context.perform { [context] in + do { + let fetchRequest = NSFetchRequest(entityName: ManagedObject.className()) + fetchRequest.predicate = predicate + + let fetchResults = try context.fetch(fetchRequest) + guard fetchResults.count <= 1 else { throw CoreDataStoreError.multipleObjectsFound } + guard let managedObject = fetchResults.first as? ManagedObject else { + throw CoreDataStoreError.objectNotFound + } + + try managedObject.update(with: value) + try context.save() + + mainQueueCompletion(nil) + } catch { + mainQueueCompletion(error) + } + } + } + + func update(objectWithId id: NSManagedObjectID, with value: Value, completionHandler: ((Error?) -> Void)?) { + guard let context = writeContext() else { return } + + func mainQueueCompletion(_ error: Error?) { + guard completionHandler != nil else { return } + DispatchQueue.main.async { + completionHandler?(error) + } + } + + context.perform { [context] in + do { + guard let managedObject = try? context.existingObject(with: id) as? ManagedObject else { + assertionFailure("CoreDataStore: Failed to get Managed Object from the context") + throw CoreDataStoreError.objectNotFound + } + + try managedObject.update(with: value) + try context.save() + + mainQueueCompletion(nil) + } catch { + mainQueueCompletion(error) + } + } + } + + func remove(objectsWithPredicate predicate: NSPredicate, + identifiedBy identifierKeyPath: KeyPath, + completionHandler: ((Result<[T], Error>) -> Void)?) { + guard let context = self.writeContext() else { return } + + func mainQueueCompletion(_ result: Result<[T], Error>) { + guard completionHandler != nil else { return } + DispatchQueue.main.async { + completionHandler?(result) + } + } + + context.perform { [context] in + do { + let fetchRequest = NSFetchRequest(entityName: ManagedObject.className()) + fetchRequest.predicate = predicate + + let fetchResults = try context.fetch(fetchRequest) + var removedIds = [T]() + removedIds.reserveCapacity(fetchResults.count) + for result in fetchResults { + guard let managedObject = result as? ManagedObject else { continue } + removedIds.append(managedObject[keyPath: identifierKeyPath]) + context.delete(managedObject) + } + + try context.save() + mainQueueCompletion(.success(removedIds)) + } catch { + mainQueueCompletion(.failure(error)) + } + } + } + + func remove(objectsWithPredicate predicate: NSPredicate, + completionHandler: ((Result<[NSManagedObjectID], Error>) -> Void)?) { + remove(objectsWithPredicate: predicate, identifiedBy: \ManagedObject.objectID, completionHandler: completionHandler) + } + + func remove(objectWithId id: NSManagedObjectID, completionHandler: ((Error?) -> Void)?) { + guard let context = writeContext() else { return } + func mainQueueCompletion(error: Error?) { + guard completionHandler != nil else { return } + DispatchQueue.main.async { + completionHandler?(error) + } + } + + context.perform { [context] in + do { + guard let managedObject = try? context.existingObject(with: id) else { + assertionFailure("CoreDataStore: Failed to get Managed Object from the context") + throw CoreDataStoreError.objectNotFound + } + + context.delete(managedObject) + + try context.save() + mainQueueCompletion(error: nil) + } catch { + assertionFailure("CoreDataStore: Saving of context failed") + mainQueueCompletion(error: error) + } + } + } + + func clear(completionHandler: ((Error?) -> Void)?) { + guard let context = writeContext() else { return } + func mainQueueCompletion(error: Error?) { + guard completionHandler != nil else { return } + DispatchQueue.main.async { + completionHandler?(error) + } + } + + context.perform { [context] in + let deleteRequest = NSFetchRequest(entityName: ManagedObject.className()) + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: deleteRequest) + + batchDeleteRequest.resultType = .resultTypeObjectIDs + + do { + _=try context.execute(batchDeleteRequest) + mainQueueCompletion(error: nil) + } catch { + mainQueueCompletion(error: error) + } + } + } + +} diff --git a/DuckDuckGo/Common/Extensions/UserDefaultsExtension.swift b/DuckDuckGo/Common/Extensions/DictionaryExtension.swift similarity index 69% rename from DuckDuckGo/Common/Extensions/UserDefaultsExtension.swift rename to DuckDuckGo/Common/Extensions/DictionaryExtension.swift index 20a670166d..1deb644d41 100644 --- a/DuckDuckGo/Common/Extensions/UserDefaultsExtension.swift +++ b/DuckDuckGo/Common/Extensions/DictionaryExtension.swift @@ -1,8 +1,7 @@ // -// UserDefaultsExtension.swift -// DuckDuckGo +// DictionaryExtension.swift // -// Copyright © 2017 DuckDuckGo. All rights reserved. +// Copyright © 2021 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,12 +15,13 @@ // See the License for the specific language governing permissions and // limitations under the License. // + import Foundation -extension UserDefaults { +extension Dictionary { - public func bool(forKey key: String, defaultValue: Bool) -> Bool { - return object(forKey: key) as? Bool ?? defaultValue + mutating func updateInPlace(key: Key, update: (inout Value?) throws -> T) rethrows -> T { + try update(&self[key]) } } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 9deed9a4d1..ee3615cd3c 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -32,6 +32,7 @@ public struct UserDefaultsWrapper { case configStoragePrivacyConfigurationEtag = "config.storage.privacyconfiguration.etag" case fireproofDomains = "com.duckduckgo.fireproofing.allowedDomains" + case unprotectedDomains = "com.duckduckgo.contentblocker.unprotectedDomains" case defaultBrowserDismissed = "browser.default.dismissed" @@ -76,8 +77,9 @@ public struct UserDefaultsWrapper { public var wrappedValue: T { get { - if let storedValue = UserDefaults.standard.object(forKey: key.rawValue) as? T { - return storedValue + if let storedValue = UserDefaults.standard.object(forKey: key.rawValue), + let typedValue = storedValue as? T { + return typedValue } if setIfEmpty { diff --git a/DuckDuckGo/ContentBlocker/ContentBlocking.swift b/DuckDuckGo/ContentBlocker/ContentBlocking.swift index dd220e6c85..3e4b90a4be 100644 --- a/DuckDuckGo/ContentBlocker/ContentBlocking.swift +++ b/DuckDuckGo/ContentBlocker/ContentBlocking.swift @@ -21,14 +21,14 @@ import BrowserServicesKit import Combine import os.log -// swiftlint:disable line_length final class ContentBlocking { - static let privacyConfigurationManager = PrivacyConfigurationManager(fetchedETag: DefaultConfigurationStorage.shared.loadEtag(for: .privacyConfiguration), - fetchedData: DefaultConfigurationStorage.shared.loadData(for: .privacyConfiguration), - embeddedDataProvider: AppPrivacyConfigurationDataProvider(), - localProtection: DomainsProtectionUserDefaultsStore(), - errorReporting: debugEvents) + static let privacyConfigurationManager + = PrivacyConfigurationManager(fetchedETag: DefaultConfigurationStorage.shared.loadEtag(for: .privacyConfiguration), + fetchedData: DefaultConfigurationStorage.shared.loadData(for: .privacyConfiguration), + embeddedDataProvider: AppPrivacyConfigurationDataProvider(), + localProtection: LocalUnprotectedDomains.shared, + errorReporting: debugEvents) static let contentBlockingUpdating = ContentBlockingUpdating() @@ -104,46 +104,3 @@ final class ContentBlockingUpdating: ContentBlockerRulesUpdating { } } - -private class DomainsProtectionUserDefaultsStore: DomainsProtectionStore { - - private struct Keys { - static let unprotectedDomains = "com.duckduckgo.contentblocker.unprotectedDomains" - } - - private var userDefaults: UserDefaults? { - return UserDefaults() - } - - public private(set) var unprotectedDomains: Set { - get { - guard let data = userDefaults?.data(forKey: Keys.unprotectedDomains) else { return Set() } - guard let unprotectedDomains = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSSet.self, from: data) as? Set else { - return Set() - } - return unprotectedDomains - } - set(newUnprotectedDomain) { - guard let data = try? NSKeyedArchiver.archivedData(withRootObject: newUnprotectedDomain, requiringSecureCoding: false) else { return } - userDefaults?.set(data, forKey: Keys.unprotectedDomains) - } - } - - public func isHostUnprotected(forDomain domain: String) -> Bool { - return unprotectedDomains.contains(domain) - } - - public func disableProtection(forDomain domain: String) { - var domains = unprotectedDomains - domains.insert(domain) - unprotectedDomains = domains - } - - public func enableProtection(forDomain domain: String) { - var domains = unprotectedDomains - domains.remove(domain) - unprotectedDomains = domains - } -} - -// swiftlint:enable line_length diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift index a05e35d942..2d2fee1330 100644 --- a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift @@ -17,6 +17,8 @@ // import Foundation +import CoreData +import os.log internal class FireproofDomains { @@ -27,56 +29,111 @@ internal class FireproofDomains { } static let shared = FireproofDomains() + private let store: FireproofDomainsStore - @UserDefaultsWrapper(key: .fireproofDomains, defaultValue: []) - private(set) var fireproofDomains: [String] { + @UserDefaultsWrapper(key: .fireproofDomains, defaultValue: nil) + private var legacyUserDefaultsFireproofDomains: [String]? + + private lazy var container: FireproofDomainsContainer = loadFireproofDomains() { didSet { NotificationCenter.default.post(name: Constants.allowedDomainsChangedNotification, object: self) } } + var fireproofDomains: [String] { + container.domains + } + + init(store: FireproofDomainsStore = FireproofDomainsStore(tableName: "FireproofDomains")) { + self.store = store + } + + private func loadFireproofDomains() -> FireproofDomainsContainer { + dispatchPrecondition(condition: .onQueue(.main)) + do { + if let domains = legacyUserDefaultsFireproofDomains?.map({ $0.dropWWW() }), + !domains.isEmpty { + + var container = FireproofDomainsContainer() + do { + let added = try store.add(Set(domains)) + for (domain, id) in added { + try container.add(domain: domain, withId: id) + } + + self.legacyUserDefaultsFireproofDomains = nil + } catch {} + + return container + } + + return try store.load() + } catch { + os_log("FireproofDomainsStore: Failed to load Fireproof Domains", type: .error) + return FireproofDomainsContainer() + } + } + func toggle(domain: String) -> Bool { + dispatchPrecondition(condition: .onQueue(.main)) if isFireproof(fireproofDomain: domain) { remove(domain: domain) return false - } else { - addToAllowed(domain: domain) - return true } + add(domain: domain) + return true } - func addToAllowed(domain: String) { + func add(domain: String) { + dispatchPrecondition(condition: .onQueue(.main)) guard !isFireproof(fireproofDomain: domain) else { + // submodains also? return } - fireproofDomains += [domain] + let domainWithoutWWW = domain.dropWWW() + do { + let id = try store.add(domainWithoutWWW) + try container.add(domain: domainWithoutWWW, withId: id) + } catch { + assertionFailure("could not add fireproof domain \(domain): \(error)") + return + } NotificationCenter.default.post(name: Constants.newFireproofDomainNotification, object: self, userInfo: [ - Constants.newFireproofDomainKey: domain + Constants.newFireproofDomainKey: domainWithoutWWW ]) } - public func isFireproof(cookieDomain: String) -> Bool { - fireproofDomains.contains { - $0 == cookieDomain - || ".\($0)" == cookieDomain - || (cookieDomain.hasPrefix(".") && $0.hasSuffix(cookieDomain)) + func remove(domain: String) { + dispatchPrecondition(condition: .onQueue(.main)) + guard let id = container.remove(domain: domain) else { + assertionFailure("fireproof domain \(domain) not found") + return } - } - func remove(domain: String) { - fireproofDomains.removeAll { - $0 == domain || $0 == "www.\(domain)" + store.remove(objectWithId: id) { error in + if let error = error { + assertionFailure("FireproofDomainsStore: Failed to remove Fireproof Domain: \(error)") + return + } } } func clearAll() { - fireproofDomains = [] + dispatchPrecondition(condition: .onQueue(.main)) + container = FireproofDomainsContainer() + store.clear() + } + + func isFireproof(cookieDomain: String) -> Bool { + let domainWithoutDotPrefix = cookieDomain.drop(prefix: ".") + return container.contains(domain: domainWithoutDotPrefix, includingSuperdomains: false) + || (cookieDomain.hasPrefix(".") && container.contains(superdomain: domainWithoutDotPrefix)) } func isFireproof(fireproofDomain domain: String) -> Bool { - return fireproofDomains.contains(where: { $0.hasSuffix(domain) || $0.dropWWW().hasSuffix(domain) }) + return container.contains(domain: domain) } func isURLFireproof(url: URL) -> Bool { diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomains.xcdatamodeld/Permissions.xcdatamodel/contents b/DuckDuckGo/Fireproofing/Model/FireproofDomains.xcdatamodeld/Permissions.xcdatamodel/contents new file mode 100644 index 0000000000..2d92f51dc7 --- /dev/null +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomains.xcdatamodeld/Permissions.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift new file mode 100644 index 0000000000..d9ca287dd9 --- /dev/null +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift @@ -0,0 +1,93 @@ +// +// FireproofDomainsContainer.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import CoreData + +struct FireproofDomainsContainer { + + struct DomainAlreadyAdded: Error {} + + private var domainsToIds = [String: NSManagedObjectID]() + + // auto-generated superdomain->subdomain where subdomain is originally fireproofed + // used for quick search for superdomains when checking in isFireproof(..) functions + // would store "name.com" -> ["mail.name.com", "news.name.com"] + // adding "admin.name.com" would add the domain to the related set + // removing "mail.name.com" would remove the domain from the related set + // when set goes empty, the record should be removed + private var superdomainsToSubdomains = [String: Set]() + + var domains: [String] { + return Array(domainsToIds.keys) + } + + @discardableResult + mutating func add(domain: String, withId id: NSManagedObjectID) throws -> String { + let domain = domain.dropWWW() + try domainsToIds.updateInPlace(key: domain) { value in + guard value == nil else { throw DomainAlreadyAdded() } + value = id + } + + let components = domain.components(separatedBy: ".") + if components.count > 2 { + for i in 1.. NSManagedObjectID? { + let domain = domain.dropWWW() + guard let idx = domainsToIds.index(forKey: domain) else { + assertionFailure("\(domain) is not Fireproof") + return nil + } + let id = domainsToIds.remove(at: idx).value + + let components = domain.components(separatedBy: ".") + guard components.count > 2 else { return id } + + for i in 1.. Bool { + let domain = domain.dropWWW() + return domainsToIds[domain] != nil || (includingSuperdomains && contains(superdomain: domain)) + } + + func contains(superdomain: String) -> Bool { + return superdomainsToSubdomains[superdomain] != nil + } + +} diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomainsStore.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomainsStore.swift new file mode 100644 index 0000000000..27ffc5eb1e --- /dev/null +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomainsStore.swift @@ -0,0 +1,43 @@ +// +// FireproofDomainsStore.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import CoreData + +typealias FireproofDomainsStore = CoreDataStore +extension FireproofDomainsStore { + + func load() throws -> FireproofDomainsContainer { + try load(into: FireproofDomainsContainer()) { + try $0.add(domain: $1.value, withId: $1.id) + } + } + +} + +extension FireproofDomainManagedObject: ValueRepresentableManagedObject { + + func update(with domain: String) { + self.domainEncrypted = domain as NSString + } + + func valueRepresentation() -> String? { + self.domainEncrypted as? String + } + +} diff --git a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift index 80d51690ab..5d48e9a40f 100644 --- a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift +++ b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift @@ -56,7 +56,7 @@ final class FireproofDomainsViewController: NSViewController { fileprivate func reloadData() { allFireproofDomains = FireproofDomains.shared.fireproofDomains.sorted { (lhs, rhs) -> Bool in - return lhs.dropWWW() < rhs.dropWWW() + return lhs < rhs } tableView.reloadData() diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index 9e2513fabd..7fcb490e97 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -100,7 +100,7 @@ final class SaveCredentialsViewController: NSViewController { Pixel.fire(.autofillItemSaved(kind: .password)) if self.fireproofCheck.state == .on { Pixel.fire(.fireproof(kind: .pwm, suggested: .pwm)) - FireproofDomains.shared.addToAllowed(domain: account.domain) + FireproofDomains.shared.add(domain: account.domain) } } @@ -126,7 +126,7 @@ final class SaveCredentialsViewController: NSViewController { alert.beginSheetModal(for: window) { response in if response == NSApplication.ModalResponse.alertFirstButtonReturn { Pixel.fire(.fireproof(kind: .pwm, suggested: .suggested)) - FireproofDomains.shared.addToAllowed(domain: host) + FireproofDomains.shared.add(domain: host) } } diff --git a/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift b/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift index a2d01a0118..b8d2ba90ba 100644 --- a/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift +++ b/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift @@ -28,7 +28,7 @@ final class HTTPSUpgrade { private let dataReloadLock = NSLock() private let store: HTTPSUpgradeStore private var bloomFilter: BloomFilterWrapper? - + init(store: HTTPSUpgradeStore = HTTPSUpgradePersistence()) { self.store = store } @@ -50,7 +50,7 @@ final class HTTPSUpgrade { completion(false) return } - + guard config.isFeature(.httpsUpgrade, enabledForDomain: host) else { completion(false) return diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 1ee56fd609..4de233e0ca 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -765,17 +765,30 @@ extension TabBarViewController: TabBarViewItemDelegate { } func tabBarViewItemFireproofSite(_ tabBarViewItem: TabBarViewItem) { - if let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url, - let host = url.host { - Pixel.fire(.fireproof(kind: .init(url: url), suggested: .manual)) - FireproofDomains.shared.addToAllowed(domain: host) + guard let indexPath = collectionView.indexPath(for: tabBarViewItem), + let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), + let url = tabViewModel.tab.content.url, + let host = url.host + else { + os_log("TabBarViewController: Failed to get url of tab bar view item", type: .error) + return } + + Pixel.fire(.fireproof(kind: .init(url: url), suggested: .manual)) + FireproofDomains.shared.add(domain: host) } func tabBarViewItemRemoveFireproofing(_ tabBarViewItem: TabBarViewItem) { - if let host = tabCollectionViewModel.selectedTabViewModel?.tab.content.url?.host { - FireproofDomains.shared.remove(domain: host) + guard let indexPath = collectionView.indexPath(for: tabBarViewItem), + let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), + let url = tabViewModel.tab.content.url, + let host = url.host + else { + os_log("TabBarViewController: Failed to get url of tab bar view item", type: .error) + return } + + FireproofDomains.shared.remove(domain: host) } func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState { diff --git a/DuckDuckGo/Unprotected Domains/KeySetDictionary.swift b/DuckDuckGo/Unprotected Domains/KeySetDictionary.swift new file mode 100644 index 0000000000..b0afaf78f9 --- /dev/null +++ b/DuckDuckGo/Unprotected Domains/KeySetDictionary.swift @@ -0,0 +1,53 @@ +// +// KeySetDictionary.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct KeySetDictionary: ExpressibleByDictionaryLiteral { + + private var dict = [Key: Value]() + private(set) var keys = Set() + + init () {} + + init(dictionaryLiteral elements: (Key, Value)...) { + for (key, value) in elements { + self[key] = value + } + } + + subscript(key: Key) -> Value? { + get { + return dict[key] + } + set { + dict[key] = newValue + if newValue != nil { + keys.insert(key) + } else { + keys.remove(key) + } + } + } + + mutating func removeValue(forKey key: Key) -> Value? { + keys.remove(key) + return dict.removeValue(forKey: key) + } + +} diff --git a/DuckDuckGo/Unprotected Domains/LocalUnprotectedDomains.swift b/DuckDuckGo/Unprotected Domains/LocalUnprotectedDomains.swift new file mode 100644 index 0000000000..3f35ebc0fa --- /dev/null +++ b/DuckDuckGo/Unprotected Domains/LocalUnprotectedDomains.swift @@ -0,0 +1,117 @@ +// +// LocalUnprotectedDomains.swift +// DuckDuckGo +// +// Copyright © 2017 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import Foundation +import CoreData +import os.log +import BrowserServicesKit + +typealias UnprotectedDomainsStore = CoreDataStore +final class LocalUnprotectedDomains: DomainsProtectionStore { + + static let shared = LocalUnprotectedDomains() + + private let store: UnprotectedDomainsStore + + @UserDefaultsWrapper(key: .unprotectedDomains, defaultValue: nil) + private var legacyUserDefaultsUnprotectedDomainsData: Data? + + private let queue = DispatchQueue(label: "unprotected.domains.queue") + + private typealias UnprotectedDomainsContainer = KeySetDictionary + lazy private var _unprotectedDomains: UnprotectedDomainsContainer = loadUnprotectedDomains() + + private var unprotectedDomainsToIds: UnprotectedDomainsContainer { + queue.sync { + _unprotectedDomains + } + } + + var unprotectedDomains: Set { + queue.sync { + _unprotectedDomains.keys + } + } + + private func modifyUnprotectedDomains(_ modify: (inout UnprotectedDomainsContainer) throws -> T) rethrows -> T { + try queue.sync { + try modify(&_unprotectedDomains) + } + } + + init(store: UnprotectedDomainsStore = UnprotectedDomainsStore(tableName: "UnprotectedDomains")) { + self.store = store + } + + private func loadUnprotectedDomains() -> UnprotectedDomainsContainer { + do { + if let data = legacyUserDefaultsUnprotectedDomainsData, + let domains = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSSet.self, from: data) as? Set, + !domains.isEmpty { + + var result = UnprotectedDomainsContainer() + do { + result = try store.add(domains).reduce(into: [:]) { $0[$1.value] = $1.id } + self.legacyUserDefaultsUnprotectedDomainsData = nil + } catch {} + + return result + } + + return try store.load(into: [:]) { $0[$1.value] = $1.id } + } catch { + os_log("UnprotectedDomainStore: Failed to load Unprotected Domains", type: .error) + return [:] + } + } + + func disableProtection(forDomain domain: String) { + do { + let id = try store.add(domain) + modifyUnprotectedDomains { $0[domain] = id } + } catch { + assertionFailure("could not add unprotected domain \(domain): \(error)") + } + } + + func enableProtection(forDomain domain: String) { + guard let id = modifyUnprotectedDomains({ $0.removeValue(forKey: domain) }) else { + assertionFailure("unprotected domain \(domain) not found") + return + } + store.remove(objectWithId: id) { error in + if let error = error { + assertionFailure("UnprotectedDomainStore: Failed to remove Unprotected Domain: \(error)") + return + } + } + } + +} + +extension UnprotectedDomainManagedObject: ValueRepresentableManagedObject { + + func valueRepresentation() -> String? { + self.domainEncrypted as? String + } + + func update(with domain: String) { + self.domainEncrypted = domain as NSString + } + +} diff --git a/DuckDuckGo/Unprotected Domains/UnprotectedDomains.xcdatamodeld/Permissions.xcdatamodel/contents b/DuckDuckGo/Unprotected Domains/UnprotectedDomains.xcdatamodeld/Permissions.xcdatamodel/contents new file mode 100644 index 0000000000..d4434b834a --- /dev/null +++ b/DuckDuckGo/Unprotected Domains/UnprotectedDomains.xcdatamodeld/Permissions.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift b/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift index 8244dc8892..1c8e46593e 100644 --- a/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift +++ b/Unit Tests/BrowserTab/Services/WebsiteDataStoreTests.swift @@ -58,7 +58,7 @@ final class WebCacheManagerTests: XCTestCase { func testWhenClearedThenCookiesWithParentDomainsAreRetained() { let logins = MockPreservedLogins(domains: [ - "www.example.com" + "example.com" ]) let cookieStore = MockHTTPCookieStore(cookies: [ @@ -88,7 +88,7 @@ final class WebCacheManagerTests: XCTestCase { func testWhenClearedThenDDGCookiesAreRetained() { let logins = MockPreservedLogins(domains: [ - "www.example.com" + "example.com" ]) let cookieStore = MockHTTPCookieStore(cookies: [ @@ -115,7 +115,7 @@ final class WebCacheManagerTests: XCTestCase { func testWhenClearedThenCookiesForLoginsAreRetained() { let logins = MockPreservedLogins(domains: [ - "www.example.com" + "example.com" ]) let cookieStore = MockHTTPCookieStore(cookies: [ @@ -192,14 +192,12 @@ final class WebCacheManagerTests: XCTestCase { class MockPreservedLogins: FireproofDomains { - let domains: [String] - - override var fireproofDomains: [String] { - return domains - } - init(domains: [String]) { - self.domains = domains + super.init(store: FireproofDomainsStoreMock()) + + for domain in domains { + super.add(domain: domain) + } } } diff --git a/Unit Tests/Common/CoreDataTestUtilities.swift b/Unit Tests/Common/CoreDataTestUtilities.swift index 3d1dd338d7..3f7a2ac6f2 100644 --- a/Unit Tests/Common/CoreDataTestUtilities.swift +++ b/Unit Tests/Common/CoreDataTestUtilities.swift @@ -30,6 +30,14 @@ final class CoreData { return createInMemoryPersistentContainer(modelName: "Permissions", bundle: Bundle(for: AppDelegate.self)) } + static func fireproofingContainer() -> NSPersistentContainer { + return createInMemoryPersistentContainer(modelName: "FireproofDomains", bundle: Bundle(for: AppDelegate.self)) + } + + static func coreDataStoreTestsContainer() -> NSPersistentContainer { + return createInMemoryPersistentContainer(modelName: "TestDataModel", bundle: Bundle(for: Self.self)) + } + static func downloadsContainer() -> NSPersistentContainer { return createInMemoryPersistentContainer(modelName: "Downloads", bundle: Bundle(for: AppDelegate.self)) } diff --git a/Unit Tests/Database/CoreDataStoreTests.swift b/Unit Tests/Database/CoreDataStoreTests.swift new file mode 100644 index 0000000000..8b87be26f4 --- /dev/null +++ b/Unit Tests/Database/CoreDataStoreTests.swift @@ -0,0 +1,163 @@ +// +// CoreDataStoreTests.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class CoreDataStoreTests: XCTestCase { + + let container = CoreData.coreDataStoreTestsContainer() + typealias Store = CoreDataStore + lazy var store = Store(context: container.viewContext, tableName: "TestDataModel") + + private func load(into result: inout [CoreDataTestStruct: NSManagedObjectID], + idValue: Store.IDValueTuple) throws { + result[idValue.value] = idValue.id + } + + func testWhenObjectIsAddedThenItMustBeLoadedFromStore() throws { + let storedId = try store.add(.init(domain: "duckduckgo.com")) + + let fireproofed = try store.load(into: .init(), self.load) + XCTAssertEqual(fireproofed, [.init(domain: "duckduckgo.com"): storedId]) + } + + func testWhenObjectIsRemovedThenItShouldntBeLoadedFromStore() throws { + let storedId1 = try store.add(.init(domain: "duckduckgo.com")) + let storedId2 = try store.add(.init(domain: "otherdomain.com")) + + let e = expectation(description: "object removed") + store.remove(objectWithId: storedId2) { [store] error in + XCTAssertNil(error) + let fireproofed = try? store.load(into: .init(), self.load) + XCTAssertEqual(fireproofed, [.init(domain: "duckduckgo.com"): storedId1]) + e.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testWhenObjectIsRemovedWithPredicateThenItShouldntBeLoadedFromStore() throws { + let storedId1 = try store.add(.init(domain: "duckduckgo.com", testAttribute: "a")) + let storedId2 = try store.add(.init(domain: "otherdomain.com", testAttribute: "b")) + + let e = expectation(description: "object removed") + store.remove(objectsWithPredicate: NSPredicate(format: #keyPath(TestManagedObject.testAttribute) + " == %@", "b")) { [store] result in + switch result { + case .success(let ids): + XCTAssertEqual(ids, [storedId2]) + case .failure(let error): + XCTFail("unexpected error \(error)") + } + + let fireproofed = try? store.load(into: .init(), self.load) + XCTAssertEqual(fireproofed, [.init(domain: "duckduckgo.com", testAttribute: "a"): storedId1]) + e.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testWhenObjectIsUpdatedThenIstLoadedWithNewValue() throws { + let storedId1 = try store.add(.init(domain: "duckduckgo.com")) + let storedId2 = try store.add(.init(domain: "otherdomain.com")) + + let e = expectation(description: "object removed") + store.update(objectWithId: storedId1, with: .init(domain: "www.duckduckgo.com", testAttribute: "a")) { [store] error in + XCTAssertNil(error) + + let fireproofed = try? store.load(into: .init(), self.load) + XCTAssertEqual(fireproofed, [.init(domain: "www.duckduckgo.com", testAttribute: "a"): storedId1, + .init(domain: "otherdomain.com"): storedId2]) + e.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testWhenObjectIsUpdatedWithPredicateThenIstLoadedWithNewValue() throws { + let storedId1 = try store.add(.init(domain: "duckduckgo.com", testAttribute: "a")) + let storedId2 = try store.add(.init(domain: "otherdomain.com", testAttribute: "b")) + + let e = expectation(description: "object removed") + store.update(objectWithPredicate: NSPredicate(format: #keyPath(TestManagedObject.testAttribute) + " == %@", "a"), + with: .init(domain: "www.duckduckgo.com", testAttribute: "a")) { [store] error in + XCTAssertNil(error) + + let fireproofed = try? store.load(into: .init(), self.load) + XCTAssertEqual(fireproofed, [.init(domain: "www.duckduckgo.com", testAttribute: "a"): storedId1, + .init(domain: "otherdomain.com", testAttribute: "b"): storedId2]) + e.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testWhenUpdateWithPredicateFailsThenErrorIsReturned() throws { + let storedId1 = try store.add(.init(domain: "duckduckgo.com", testAttribute: "a")) + let storedId2 = try store.add(.init(domain: "otherdomain.com", testAttribute: "b")) + + let e = expectation(description: "object removed") + store.update(objectWithPredicate: NSPredicate(format: #keyPath(TestManagedObject.testAttribute) + " == %@", "c"), + with: .init(domain: "www.duckduckgo.com", testAttribute: "c")) { [store] error in + XCTAssertEqual(error as? CoreDataStoreError, CoreDataStoreError.objectNotFound) + let fireproofed = try? store.load(into: .init(), self.load) + XCTAssertEqual(fireproofed, [.init(domain: "duckduckgo.com", testAttribute: "a"): storedId1, + .init(domain: "otherdomain.com", testAttribute: "b"): storedId2]) + e.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testWhenObjectsAreClearedThenOnlyExceptionsRemain() throws { + _=try store.add(.init(domain: "duckduckgo.com")) + _=try store.add(.init(domain: "otherdomain.com")) + _=try store.add(.init(domain: "wikipedia.org")) + _=try store.add(.init(domain: "fireproofing.site")) + + let e = expectation(description: "store cleared") + store.clear { [store] error in + XCTAssertNil(error) + + let fireproofed = try! store.load(into: .init(), self.load) // swiftlint:disable:this force_try + + XCTAssertEqual(fireproofed, [:]) + + e.fulfill() + } + waitForExpectations(timeout: 1) + } + +} + +public struct CoreDataTestStruct: Hashable { + var domain: String + var testAttribute: String? +} +extension TestManagedObject: ValueRepresentableManagedObject { + + public func update(with val: CoreDataTestStruct) { + self.domainEncrypted = val.domain as NSString + self.testAttribute = val.testAttribute + } + + public func valueRepresentation() -> CoreDataTestStruct? { + guard let domain = self.domainEncrypted as? String else { return nil } + return CoreDataTestStruct(domain: domain, testAttribute: self.testAttribute) + } + +} diff --git a/Unit Tests/Database/TestDataModel.xcdatamodeld/Permissions.xcdatamodel/contents b/Unit Tests/Database/TestDataModel.xcdatamodeld/Permissions.xcdatamodel/contents new file mode 100644 index 0000000000..cf452775ce --- /dev/null +++ b/Unit Tests/Database/TestDataModel.xcdatamodeld/Permissions.xcdatamodel/contents @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Unit Tests/Fireproofing/FireproofDomainsStoreMock.swift b/Unit Tests/Fireproofing/FireproofDomainsStoreMock.swift new file mode 100644 index 0000000000..f051ba48d5 --- /dev/null +++ b/Unit Tests/Fireproofing/FireproofDomainsStoreMock.swift @@ -0,0 +1,84 @@ +// +// FireproofDomainsStoreMock.swift +// +// Copyright © 2021 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import DuckDuckGo_Privacy_Browser + +final class FireproofDomainsStoreMock: FireproofDomainsStore { + + var domains = [String: NSManagedObjectID]() + var error: Error? + + enum CallHistoryItem: Equatable { + case load + case remove(NSManagedObjectID) + case add(domains: [String]) + case clear + } + + var history = [CallHistoryItem]() + + init() { + super.init(context: nil, tableName: "") + } + + override func load(objectsWithPredicate predicate: NSPredicate? = nil, + sortDescriptors: [NSSortDescriptor]? = nil, + into initialResult: Result, + _ accumulate: (inout Result, IDValueTuple) throws -> Void) throws -> Result { + + history.append(.load) + if let error = error { + throw error + } + + var result = initialResult + for (domain, id) in domains { + try accumulate(&result, (id, domain)) + } + return result + } + + override func add(_ fireproofDomains: S) throws + -> [(value: Value, id: NSManagedObjectID)] where S: Sequence, String == S.Element { + + history.append(.add(domains: Array(fireproofDomains))) + if let error = error { + throw error + } + var result = [(value: String, id: NSManagedObjectID)]() + for domain in fireproofDomains { + result.append( (domain, .init()) ) + domains[domain] = result.last!.id + } + return result + } + + override func remove(objectWithId id: NSManagedObjectID, completionHandler: ((Error?) -> Void)? = nil) { + history.append(.remove(id)) + domains[domains.first(where: { $0.value == id })!.key] = nil + completionHandler?(nil) + } + + override func clear(completionHandler: ((Error?) -> Void)? = nil) { + history.append(.clear) + domains = [:] + completionHandler?(nil) + } + +} diff --git a/Unit Tests/Fireproofing/FireproofDomainsTests.swift b/Unit Tests/Fireproofing/FireproofDomainsTests.swift index e5ad74d67f..b96e5641ef 100644 --- a/Unit Tests/Fireproofing/FireproofDomainsTests.swift +++ b/Unit Tests/Fireproofing/FireproofDomainsTests.swift @@ -20,39 +20,104 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser final class FireproofDomainsTests: XCTestCase { + let store = FireproofDomainsStoreMock() + lazy var logins: FireproofDomains = FireproofDomains(store: store) override func setUp() { UserDefaultsWrapper.clearAll() } func testWhenFireproofDomainsContainsFireproofedDomainThenReturnsTrue() { - let logins = FireproofDomains() XCTAssertFalse(logins.isFireproof(fireproofDomain: "example.com")) - logins.addToAllowed(domain: "example.com") - XCTAssertTrue(logins.isFireproof(fireproofDomain: "example.com")) + logins.add(domain: "example.com") + XCTAssertTrue(logins.isFireproof(fireproofDomain: "www.example.com")) + } + + func testWhenFireproofDomainsContainsFireproofedDomainThenIsURLFireproofReturnsTrue() { + XCTAssertFalse(logins.isFireproof(fireproofDomain: "example.com")) + logins.add(domain: "example.com") + XCTAssertTrue(logins.isURLFireproof(url: URL(string: "http://www.example.com/example")!)) + } + + func testWhenFireproofDomainsDoesNotContainDomainThenIsURLFireproofReturnsFalse() { + XCTAssertFalse(logins.isFireproof(fireproofDomain: "thisisexample.com")) + logins.add(domain: "thisisexample.com") + XCTAssertFalse(logins.isURLFireproof(url: URL(string: "http://www.example.com/example")!)) + } + + func testWhenFireproofDomainsContainsCookieDomainThenIsCookieDomainFireproofReturnsTrue() { + logins.add(domain: "www.example.com") + XCTAssertTrue(logins.isFireproof(cookieDomain: "example.com")) + } + + func testWhenFireproofDomainsContainsCookieDomainThenDotPrefixedIsCookieDomainFireproofReturnsTrue() { + logins.add(domain: "www.example.com") + XCTAssertTrue(logins.isFireproof(cookieDomain: ".example.com")) + } + + func testWhenFireproofDomainsContainsCookieSubdomainThenDotPrefixedIsCookieDomainFireproofReturnsTrue() { + logins.add(domain: "www.sub.example.com") + XCTAssertTrue(logins.isFireproof(cookieDomain: ".example.com")) + } + + func testWhenFireproofDomainsDoesNotContainCookieDomainThenIsCookieDomainFireproofReturnsFalse() { + logins.add(domain: "thisisexample.com") + XCTAssertFalse(logins.isFireproof(cookieDomain: "example.com")) } func testWhenNewThenFireproofDomainsIsEmpty() { - let logins = FireproofDomains() XCTAssertTrue(logins.fireproofDomains.isEmpty) } + func testWhenFireproofedDomainsInUserDefaultsThenMigrationIsPerformed() { + var udw = UserDefaultsWrapper<[String]?>(key: .fireproofDomains, defaultValue: nil) + udw.wrappedValue = ["example.com", "www.secondexample.com"] + XCTAssertEqual(logins.fireproofDomains.sorted(), ["example.com", "secondexample.com"]) + XCTAssertNil(udw.wrappedValue) + } + + func testWhenInitWithErrorThenFireproofDomainsWorkCorrectly() { + struct TestError: Error {} + store.error = TestError() + XCTAssertTrue(logins.fireproofDomains.isEmpty) + store.error = nil + logins.add(domain: "example.com") + XCTAssertEqual(logins.fireproofDomains, ["example.com"]) + } + + func testWhenFireproofedDomainsInStoreThenTheyAreLoaded() { + var udw = UserDefaultsWrapper<[String]?>(key: .fireproofDomains, defaultValue: nil) + udw.wrappedValue = [] + store.domains = ["example.com": .init(), "secondexample.com": .init()] + XCTAssertEqual(logins.fireproofDomains.sorted(), ["example.com", "secondexample.com"]) + XCTAssertEqual(udw.wrappedValue, []) + } + func testWhenRemovingDomainThenOtherDomainsAreNotRemoved() { - let logins = FireproofDomains() - logins.addToAllowed(domain: "example.com") - logins.addToAllowed(domain: "secondexample.com") + logins.add(domain: "example.com") + logins.add(domain: "www.secondexample.com") XCTAssertTrue(logins.isFireproof(fireproofDomain: "example.com")) XCTAssertTrue(logins.isFireproof(fireproofDomain: "secondexample.com")) logins.remove(domain: "secondexample.com") - XCTAssertTrue(logins.isFireproof(fireproofDomain: "example.com")) + XCTAssertTrue(logins.isFireproof(fireproofDomain: "www.example.com")) XCTAssertFalse(logins.isFireproof(fireproofDomain: "secondexample.com")) XCTAssertFalse(logins.fireproofDomains.isEmpty) } + func testWhenTogglingFireproofDomainThenItIsRemoved() { + logins.add(domain: "www.example.com") + XCTAssertFalse(logins.toggle(domain: "example.com")) + XCTAssertFalse(logins.isFireproof(fireproofDomain: "example.com")) + } + + func testWhenTogglingNotFireproofedDomainThenItIsAdded() { + XCTAssertTrue(logins.toggle(domain: "www.example.com")) + XCTAssertTrue(logins.isFireproof(fireproofDomain: "www.example.com")) + } + func testWhenClearAllIsCalledThenAllDomainsAreRemoved() { - let logins = FireproofDomains() - logins.addToAllowed(domain: "example.com") + logins.add(domain: "example.com") XCTAssertTrue(logins.isFireproof(fireproofDomain: "example.com")) logins.clearAll() @@ -62,11 +127,10 @@ final class FireproofDomainsTests: XCTestCase { func testWhenAddingDuplicateDomainsThenSubsequentDomainsAreIgnored() { let domain = "example.com" - let logins = FireproofDomains() - logins.addToAllowed(domain: domain) + logins.add(domain: domain) XCTAssertTrue(logins.isFireproof(fireproofDomain: domain)) - logins.addToAllowed(domain: domain) + logins.add(domain: domain) XCTAssertTrue(logins.isFireproof(fireproofDomain: domain)) XCTAssertEqual(logins.fireproofDomains, [domain]) diff --git a/Unit Tests/History/Model/HistoryCoordinatorTests.swift b/Unit Tests/History/Model/HistoryCoordinatorTests.swift index c858b1ba4d..dcdc02c8af 100644 --- a/Unit Tests/History/Model/HistoryCoordinatorTests.swift +++ b/Unit Tests/History/Model/HistoryCoordinatorTests.swift @@ -128,8 +128,8 @@ class HistoryCoordinatorTests: XCTestCase { XCTAssert(historyCoordinator.history!.count == 4) - let fireproofDomains = FireproofDomains() - fireproofDomains.addToAllowed(domain: fireproofDomain) + let fireproofDomains = FireproofDomains(store: FireproofDomainsStoreMock()) + fireproofDomains.add(domain: fireproofDomain) historyCoordinator.burn(except: fireproofDomains) { XCTAssert(historyStoringMock.removeEntriesArray.count == 3) } diff --git a/Unit Tests/Permissions/PermissionManagerTests.swift b/Unit Tests/Permissions/PermissionManagerTests.swift index 0947320820..05b7bcf668 100644 --- a/Unit Tests/Permissions/PermissionManagerTests.swift +++ b/Unit Tests/Permissions/PermissionManagerTests.swift @@ -199,8 +199,8 @@ final class PermissionManagerTests: XCTestCase { func testWhenPermissionsBurnedThenTheyAreCleared() { store.permissions = [.entity1, .entity2] - let fireproofDomains = FireproofDomains() - fireproofDomains.addToAllowed(domain: PermissionEntity.entity1.domain) + let fireproofDomains = FireproofDomains(store: FireproofDomainsStoreMock()) + fireproofDomains.add(domain: PermissionEntity.entity1.domain) manager.burnPermissions(except: fireproofDomains) {} diff --git a/Unit Tests/Statistics/PixelArgumentsTests.swift b/Unit Tests/Statistics/PixelArgumentsTests.swift index 567b128255..4e52fc2c5b 100644 --- a/Unit Tests/Statistics/PixelArgumentsTests.swift +++ b/Unit Tests/Statistics/PixelArgumentsTests.swift @@ -33,7 +33,7 @@ class PixelArgumentsTests: XCTestCase { bookmarkManager = LocalBookmarkManager(bookmarkStore: bookmarkStore, faviconManagement: FaviconManagerMock()) bookmarkManager.loadBookmarks() UserDefaultsWrapper.clearAll() - fireproofDomains = FireproofDomains() + fireproofDomains = FireproofDomains(store: FireproofDomainsStoreMock()) pixelDataStore = PixelStoreMock() } @@ -148,7 +148,7 @@ class PixelArgumentsTests: XCTestCase { // MARK: IsBookmarkFireproofed func testWhenInitWithFireproofDomainThenIsBookmarkFireproofedIsFireproofed() { - fireproofDomains.addToAllowed(domain: "duckduckgo.com") + fireproofDomains.add(domain: "duckduckgo.com") let url = URL(string: "https://duckduckgo.com/?q=search")! let fp = Pixel.Event.IsBookmarkFireproofed(url: url, fireproofDomains: fireproofDomains) XCTAssertEqual(fp, .fireproofed) From 32dbb4e4c4a037aa34742c1738e60a0ad4adc48e Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 17 Jan 2022 21:03:11 +0700 Subject: [PATCH 9/9] Version 0.18.3 --- DuckDuckGo.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ff1b1c6be5..d744a05b89 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -4529,7 +4529,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.18.2; + CURRENT_PROJECT_VERSION = 0.18.3; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -4537,7 +4537,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.18.2; + MARKETING_VERSION = 0.18.3; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -4801,7 +4801,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.18.2; + CURRENT_PROJECT_VERSION = 0.18.3; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -4809,7 +4809,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.18.2; + MARKETING_VERSION = 0.18.3; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -4829,7 +4829,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.18.2; + CURRENT_PROJECT_VERSION = 0.18.3; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -4837,7 +4837,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.18.2; + MARKETING_VERSION = 0.18.3; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -4961,7 +4961,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.18.2; + CURRENT_PROJECT_VERSION = 0.18.3; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "REVIEW=1"; @@ -4970,7 +4970,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.18.2; + MARKETING_VERSION = 0.18.3; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.review; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "DuckDuckGo Review"; @@ -5110,7 +5110,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.18.2; + CURRENT_PROJECT_VERSION = 0.18.3; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "BETA=1"; @@ -5119,7 +5119,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.18.2; + MARKETING_VERSION = 0.18.3; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "DuckDuckGo Non-Production";