From 516bcde08957371e647fbf26cf2c907f558d2083 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 12 Dec 2022 23:09:56 +0600 Subject: [PATCH 01/29] TabExtensions (#839) Task/Issue: https://app.asana.com/0/72649045549333/1202061524436301/f Tab Extensions Tech Design: https://app.asana.com/0/481882893211075/1203268245242138/f Dependency Injection Tech Design: https://app.asana.com/0/481882893211075/1203329703512228/f BSK PR: https://github.com/duckduckgo/BrowserServicesKit/pull/186/files * Dependency Provider (implemented for Tab) * TabExtensions --- DuckDuckGo.xcodeproj/project.pbxproj | 106 +++- DuckDuckGo/App Delegate/AppDelegate.swift | 38 +- .../AdClickAttributionTabExtension.swift | 181 +++++++ .../Extensions/AutofillTabExtension.swift | 161 ++++++ .../ContextMenuManager.swift | 50 +- .../Extensions/FindInPageTabExtension.swift | 75 +++ .../Extensions/HoveredLinkTabExtension.swift | 63 +++ .../Extensions/TabExtensions.swift | 126 +++++ .../Browser Tab/Model/NewWindowPolicy.swift | 9 + .../Browser Tab/Model/Tab+UIDelegate.swift | 31 +- DuckDuckGo/Browser Tab/Model/Tab.swift | 495 ++++++------------ .../Model/TabExtensionsBuilder.swift | 92 ++++ .../Model/UserContentUpdating.swift | 19 +- .../ContextMenuUserScript.swift | 1 - .../DebugUserScript.swift | 0 .../HoverUserScript.swift | 0 .../PrintingUserScript.swift | 5 +- .../{Model => UserScripts}/UserScripts.swift | 0 .../View/BrowserTabViewController.swift | 123 +++-- .../Browser Tab/ViewModel/TabViewModel.swift | 15 +- .../Common/Extensions/OptionalExtension.swift | 9 + .../Common/Extensions/StringExtension.swift | 6 + .../Extensions/WKFrameInfoExtension.swift | 32 ++ .../Extensions}/WKMenuItemIdentifier.swift | 2 +- .../WKWebViewConfigurationExtensions.swift | 22 +- .../Extensions/WKWebViewExtension.swift | 5 + .../Utilities/UserDefaultsWrapper.swift | 2 +- .../Configuration/ConfigurationManager.swift | 4 +- .../Content Blocker/ContentBlocking.swift | 72 ++- .../ScriptSourceProviding.swift | 43 +- DuckDuckGo/Find In Page/FindInPageModel.swift | 41 +- .../Find In Page/FindInPageUserScript.swift | 24 - .../FindInPageViewController.swift | 2 +- .../Model/HomePageRecentlyVisitedModel.swift | 6 +- DuckDuckGo/Main/View/MainViewController.swift | 20 +- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- .../AddressBarButtonsViewController.swift | 117 +++-- .../Navigation Bar/View/MoreOptionsMenu.swift | 2 +- .../View/NavigationBarViewController.swift | 13 +- .../Model/PreferencesSection.swift | 4 +- .../Model/PreferencesSidebarModel.swift | 16 +- .../View/PreferencesViewController.swift | 2 +- .../ContentBlockingRulesUpdateObserver.swift | 2 +- .../Smarter Encryption/PrivacyFeatures.swift | 33 +- .../Tab+NSSecureCoding.swift | 4 + .../View/WindowControllersManager.swift | 6 + DuckDuckGo/Windows/View/WindowsManager.swift | 6 +- DuckDuckGo/Youtube Player/PrivatePlayer.swift | 28 +- .../AutoconsentBackgroundTests.swift | 64 ++- .../AutoconsentMessageProtocolTests.swift | 18 +- .../ContentBlockerRulesManagerMock.swift | 10 +- .../Content Blocker/ContentBlockingMock.swift | 115 ++++ .../ContentBlockingUpdatingTests.swift | 12 +- .../EmptyAttributionRulesProver.swift | 31 ++ .../RecentlyVisitedSiteModelTests.swift | 23 +- .../PreferencesSidebarModelTests.swift | 5 + .../PrivatePlayerPreferencesTests.swift | 10 - .../Youtube Player/PrivatePlayerTests.swift | 5 +- 58 files changed, 1731 insertions(+), 677 deletions(-) create mode 100644 DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift create mode 100644 DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift rename DuckDuckGo/Browser Tab/{Model => Extensions}/ContextMenuManager.swift (93%) create mode 100644 DuckDuckGo/Browser Tab/Extensions/FindInPageTabExtension.swift create mode 100644 DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift create mode 100644 DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift create mode 100644 DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift rename DuckDuckGo/Browser Tab/{Model => UserScripts}/ContextMenuUserScript.swift (99%) rename DuckDuckGo/Browser Tab/{Model => UserScripts}/DebugUserScript.swift (100%) rename DuckDuckGo/Browser Tab/{Model => UserScripts}/HoverUserScript.swift (100%) rename DuckDuckGo/Browser Tab/{Model => UserScripts}/PrintingUserScript.swift (83%) rename DuckDuckGo/Browser Tab/{Model => UserScripts}/UserScripts.swift (100%) create mode 100644 DuckDuckGo/Common/Extensions/WKFrameInfoExtension.swift rename DuckDuckGo/{Browser Tab/Model => Common/Extensions}/WKMenuItemIdentifier.swift (99%) create mode 100644 Unit Tests/Content Blocker/ContentBlockingMock.swift create mode 100644 Unit Tests/Content Blocker/EmptyAttributionRulesProver.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d716295228..b755ab1a91 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -673,6 +673,7 @@ B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */; }; B642738227B65BAC0005DFD1 /* SecureVaultErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */; }; B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */; }; + B647EFBB2922584B00BA628D /* AdClickAttributionTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B647EFBA2922584B00BA628D /* AdClickAttributionTabExtension.swift */; }; B64C84DE2692D7400048FEBE /* PermissionAuthorization.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */; }; B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C84E22692DC9F0048FEBE /* PermissionAuthorizationViewController.swift */; }; B64C84EB2692DD650048FEBE /* PermissionAuthorizationPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C84EA2692DD650048FEBE /* PermissionAuthorizationPopover.swift */; }; @@ -778,6 +779,11 @@ B6A9E4A3261475C70067D1B9 /* AppUsageActivityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E4A2261475C70067D1B9 /* AppUsageActivityMonitor.swift */; }; B6AAAC2D260330580029438D /* PublishedAfter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AAAC2C260330580029438D /* PublishedAfter.swift */; }; B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AAAC3D26048F690029438D /* RandomAccessCollectionExtension.swift */; }; + B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AE39F029373AF200C37AA4 /* EmptyAttributionRulesProver.swift */; }; + B6AE39F329374AEC00C37AA4 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B6AE39F229374AEC00C37AA4 /* OHHTTPStubs */; }; + B6AE39F529374AEC00C37AA4 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B6AE39F429374AEC00C37AA4 /* OHHTTPStubsSwift */; }; + B6AE39F629374B8E00C37AA4 /* ContentBlockerRulesManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B610F2E727AA397100FCEBE9 /* ContentBlockerRulesManagerMock.swift */; }; + B6AE39F729374B9900C37AA4 /* PrivatePlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714B1E828EDBAAB0056C57A /* PrivatePlayerTests.swift */; }; B6AE74342609AFCE005B9B1A /* ProgressEstimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6AE74332609AFCE005B9B1A /* ProgressEstimationTests.swift */; }; B6B1E87B26D381710062C350 /* DownloadListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E87A26D381710062C350 /* DownloadListCoordinator.swift */; }; B6B1E87E26D5DA0E0062C350 /* DownloadsPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E87D26D5DA0E0062C350 /* DownloadsPopover.swift */; }; @@ -791,7 +797,15 @@ 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 */; }; + B6BDD9EE29406DFA00F68088 /* WKFrameInfoExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDD9ED29406DFA00F68088 /* WKFrameInfoExtension.swift */; }; + B6BDD9F529409DDD00F68088 /* ContentBlockingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDD9F429409DDD00F68088 /* ContentBlockingMock.swift */; }; + B6BDD9F62940B5B500F68088 /* ContentBlockingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDD9F429409DDD00F68088 /* ContentBlockingMock.swift */; }; + B6BDDA012942389000F68088 /* TabExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDDA002942389000F68088 /* TabExtensions.swift */; }; B6BE9FAA293F7955006363C6 /* ModalSheetCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BE9FA9293F7955006363C6 /* ModalSheetCancellable.swift */; }; + B6C00ECB292F839D009C73A6 /* AutofillTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ECA292F839D009C73A6 /* AutofillTabExtension.swift */; }; + B6C00ECD292F89D9009C73A6 /* FindInPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ECC292F89D9009C73A6 /* FindInPageTabExtension.swift */; }; + B6C00ED5292FB21E009C73A6 /* HoveredLinkTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ED4292FB21E009C73A6 /* HoveredLinkTabExtension.swift */; }; + B6C00ED7292FB4B4009C73A6 /* TabExtensionsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ED6292FB4B4009C73A6 /* TabExtensionsBuilder.swift */; }; B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B22D26E61CE70031CB7F /* DownloadViewModel.swift */; }; B6C0B23026E61D630031CB7F /* DownloadListStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B22F26E61D630031CB7F /* DownloadListStore.swift */; }; B6C0B23426E71BCD0031CB7F /* Downloads.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23226E71BCD0031CB7F /* Downloads.xcdatamodeld */; }; @@ -1552,6 +1566,7 @@ B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingMenu.swift; sourceTree = ""; }; B642738127B65BAC0005DFD1 /* SecureVaultErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureVaultErrorReporter.swift; sourceTree = ""; }; B643BF1327ABF772000BACEC /* NSWorkspaceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSWorkspaceExtension.swift; sourceTree = ""; }; + B647EFBA2922584B00BA628D /* AdClickAttributionTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdClickAttributionTabExtension.swift; sourceTree = ""; }; B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PermissionAuthorization.storyboard; sourceTree = ""; }; B64C84E22692DC9F0048FEBE /* PermissionAuthorizationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAuthorizationViewController.swift; sourceTree = ""; }; B64C84EA2692DD650048FEBE /* PermissionAuthorizationPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAuthorizationPopover.swift; sourceTree = ""; }; @@ -1660,6 +1675,7 @@ B6A9E4A2261475C70067D1B9 /* AppUsageActivityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageActivityMonitor.swift; sourceTree = ""; }; B6AAAC2C260330580029438D /* PublishedAfter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedAfter.swift; sourceTree = ""; }; B6AAAC3D26048F690029438D /* RandomAccessCollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomAccessCollectionExtension.swift; sourceTree = ""; }; + B6AE39F029373AF200C37AA4 /* EmptyAttributionRulesProver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyAttributionRulesProver.swift; sourceTree = ""; }; B6AE74332609AFCE005B9B1A /* ProgressEstimationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressEstimationTests.swift; sourceTree = ""; }; B6B1E87A26D381710062C350 /* DownloadListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListCoordinator.swift; sourceTree = ""; }; B6B1E87D26D5DA0E0062C350 /* DownloadsPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsPopover.swift; sourceTree = ""; }; @@ -1673,7 +1689,14 @@ 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 = ""; }; + B6BDD9ED29406DFA00F68088 /* WKFrameInfoExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKFrameInfoExtension.swift; sourceTree = ""; }; + B6BDD9F429409DDD00F68088 /* ContentBlockingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockingMock.swift; sourceTree = ""; }; + B6BDDA002942389000F68088 /* TabExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabExtensions.swift; sourceTree = ""; }; B6BE9FA9293F7955006363C6 /* ModalSheetCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSheetCancellable.swift; sourceTree = ""; }; + B6C00ECA292F839D009C73A6 /* AutofillTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillTabExtension.swift; sourceTree = ""; }; + B6C00ECC292F89D9009C73A6 /* FindInPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInPageTabExtension.swift; sourceTree = ""; }; + B6C00ED4292FB21E009C73A6 /* HoveredLinkTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoveredLinkTabExtension.swift; sourceTree = ""; }; + B6C00ED6292FB4B4009C73A6 /* TabExtensionsBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabExtensionsBuilder.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 = ""; }; B6C0B23326E71BCD0031CB7F /* Downloads.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Downloads.xcdatamodel; sourceTree = ""; }; @@ -1729,6 +1752,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B6AE39F329374AEC00C37AA4 /* OHHTTPStubs in Frameworks */, + B6AE39F529374AEC00C37AA4 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2423,6 +2448,8 @@ EA8AE769279FBDB20078943E /* ClickToLoadTDSTests.swift */, B610F2E527AA388100FCEBE9 /* ContentBlockingUpdatingTests.swift */, B610F2E727AA397100FCEBE9 /* ContentBlockerRulesManagerMock.swift */, + B6AE39F029373AF200C37AA4 /* EmptyAttributionRulesProver.swift */, + B6BDD9F429409DDD00F68088 /* ContentBlockingMock.swift */, ); path = "Content Blocker"; sourceTree = ""; @@ -2493,12 +2520,12 @@ 4BB88B4E25B7BA20006F6B06 /* Utilities */ = { isa = PBXGroup; children = ( + 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */, + 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */, 4BB88B5A25B7BA50006F6B06 /* Instruments.swift */, + B6AAAC2C260330580029438D /* PublishedAfter.swift */, 4BB88B4F25B7BA2B006F6B06 /* TabInstrumentation.swift */, 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */, - B6AAAC2C260330580029438D /* PublishedAfter.swift */, - 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */, - 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */, ); path = Utilities; sourceTree = ""; @@ -3130,8 +3157,6 @@ AA585D93248FD31400E9A3E2 /* Unit Tests */ = { isa = PBXGroup; children = ( - 31E163BB293A577200963C10 /* Privacy Reference Tests */, - 376718FE28E58504003A2A15 /* Youtube Player */, B6A5A28C25B962CB00AA7ADA /* App */, 85F1B0C725EF9747004792B6 /* App Delegate */, 4BF6962128C242E500D402D4 /* Autoconsent */, @@ -3160,6 +3185,7 @@ B6106BA126A7BE430013B453 /* Permissions */, 37D2377E287EFECD00BCE03B /* Pinned Tabs */, 4B0511EE262CAEB300F6079C /* Preferences */, + 31E163BB293A577200963C10 /* Privacy Reference Tests */, AA7E9174286DAFB700AB6B62 /* Recently Closed */, 858A798626A99D9000A75A42 /* Secure Vault */, B6DA440F2616C0F200DD1EC2 /* Statistics */, @@ -3167,6 +3193,7 @@ AAC9C01224CAFBB700AD1325 /* Tab Bar */, AA0877B626D515EE00B05660 /* User Agent */, 3776582B27F7163B009A6B35 /* Website Breakage Report */, + 376718FE28E58504003A2A15 /* Youtube Player */, AA585D96248FD31400E9A3E2 /* Info.plist */, ); path = "Unit Tests"; @@ -3477,6 +3504,8 @@ AA86491C24D83868001BABEE /* View */, AA86491D24D83A59001BABEE /* ViewModel */, AA86491E24D83A66001BABEE /* Model */, + B6BDD9F12940764100F68088 /* UserScripts */, + B647EFB32922539400BA628D /* Extensions */, AA512D1224D99D4900230283 /* Services */, ); path = "Browser Tab"; @@ -3508,20 +3537,14 @@ AA86491E24D83A66001BABEE /* Model */ = { isa = PBXGroup; children = ( - 856CADEF271710F400E79BB0 /* HoverUserScript.swift */, - 4B2E7D6226FF9D6500D2DB17 /* PrintingUserScript.swift */, - 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */, - 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */, - B634DBE2293C8FFF00C3C99E /* UserDialogRequest.swift */, + F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */, B634DBE4293C944700C3C99E /* NewWindowPolicy.swift */, AA9FF95824A1ECF20039E328 /* Tab.swift */, B634DBE0293C8FD500C3C99E /* Tab+Dialogs.swift */, B634DBDE293C8F7F00C3C99E /* Tab+UIDelegate.swift */, - 85AC3AEE25D5CE9800C7D2AA /* UserScripts.swift */, - B6DA06E32913ECEE00225DE2 /* ContextMenuManager.swift */, - F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */, + B6C00ED6292FB4B4009C73A6 /* TabExtensionsBuilder.swift */, 983DFB2428B67036006B7E34 /* UserContentUpdating.swift */, - B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, + B634DBE2293C8FFF00C3C99E /* UserDialogRequest.swift */, ); path = Model; sourceTree = ""; @@ -3914,8 +3937,10 @@ AA8EDF2324923E980071C2E8 /* URLExtension.swift */, AA88D14A252A557100980B4E /* URLRequestExtension.swift */, B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, + B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, + B6BDD9ED29406DFA00F68088 /* WKFrameInfoExtension.swift */, B63D466825BEB6C200874977 /* WKWebView+SessionState.swift */, B68458CC25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift */, AA92127625ADA07900600CD4 /* WKWebViewExtension.swift */, @@ -4132,6 +4157,19 @@ path = Extensions; sourceTree = ""; }; + B647EFB32922539400BA628D /* Extensions */ = { + isa = PBXGroup; + children = ( + B6BDDA002942389000F68088 /* TabExtensions.swift */, + B647EFBA2922584B00BA628D /* AdClickAttributionTabExtension.swift */, + B6C00ECA292F839D009C73A6 /* AutofillTabExtension.swift */, + B6DA06E32913ECEE00225DE2 /* ContextMenuManager.swift */, + B6C00ED4292FB21E009C73A6 /* HoveredLinkTabExtension.swift */, + B6C00ECC292F89D9009C73A6 /* FindInPageTabExtension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; B64C84DB2692D6E80048FEBE /* Permissions */ = { isa = PBXGroup; children = ( @@ -4312,6 +4350,18 @@ path = View; sourceTree = ""; }; + B6BDD9F12940764100F68088 /* UserScripts */ = { + isa = PBXGroup; + children = ( + 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */, + 4BB88B4425B7B55C006F6B06 /* DebugUserScript.swift */, + 856CADEF271710F400E79BB0 /* HoverUserScript.swift */, + 4B2E7D6226FF9D6500D2DB17 /* PrintingUserScript.swift */, + 85AC3AEE25D5CE9800C7D2AA /* UserScripts.swift */, + ); + path = UserScripts; + sourceTree = ""; + }; B6C0B23126E71A800031CB7F /* Services */ = { isa = PBXGroup; children = ( @@ -4396,6 +4446,10 @@ 4B1AD8A325FC27E200261379 /* PBXTargetDependency */, ); name = "Integration Tests"; + packageProductDependencies = ( + B6AE39F229374AEC00C37AA4 /* OHHTTPStubs */, + B6AE39F429374AEC00C37AA4 /* OHHTTPStubsSwift */, + ); productName = "Integration Tests"; productReference = 4B1AD89D25FC27E200261379 /* Integration Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -4685,13 +4739,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B6AE39F629374B8E00C37AA4 /* ContentBlockerRulesManagerMock.swift in Sources */, B662D3DF275616FF0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */, + B6AE39F729374B9900C37AA4 /* PrivatePlayerTests.swift in Sources */, 4B1AD8E225FC390B00261379 /* EncryptionMocks.swift in Sources */, B6DA06E22913AEDC00225DE2 /* TestNavigationDelegate.swift in Sources */, B31055CE27A1BA44001AC618 /* AutoconsentBackgroundTests.swift in Sources */, 4B1AD91725FC46FB00261379 /* CoreDataEncryptionTests.swift in Sources */, 7BA4727D26F01BC400EAA165 /* CoreDataTestUtilities.swift in Sources */, 4B1AD92125FC474E00261379 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, + B6BDD9F62940B5B500F68088 /* ContentBlockingMock.swift in Sources */, 4B1AD8D525FC38DD00261379 /* EncryptionKeyStoreTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4752,6 +4809,7 @@ B6DA06E42913ECEE00225DE2 /* ContextMenuManager.swift in Sources */, B693955126F04BEB0015B914 /* GradientView.swift in Sources */, 37AFCE8527DA2D3900471A10 /* PreferencesSidebar.swift in Sources */, + B6C00ED5292FB21E009C73A6 /* HoveredLinkTabExtension.swift in Sources */, AA5C8F5E2590EEE800748EB7 /* NSPointExtension.swift in Sources */, AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */, 1D43EB3A292B63B00065E5D6 /* BWRequest.swift in Sources */, @@ -4937,6 +4995,7 @@ B6DB3CF926A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift in Sources */, AAAB9116288EB46B00A057A9 /* VisitMenuItem.swift in Sources */, 4BA1A6BD258B082300F6F690 /* EncryptionKeyStore.swift in Sources */, + B6C00ED7292FB4B4009C73A6 /* TabExtensionsBuilder.swift in Sources */, 31B9226C288054D5001F55B7 /* CookieConsentPopoverManager.swift in Sources */, 4BE65474271FCD40008D1D63 /* PasswordManagementIdentityItemView.swift in Sources */, B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */, @@ -4946,6 +5005,7 @@ B63BDF7E27FDAA640072D75B /* PrivacyDashboardWebView.swift in Sources */, 37CD54CF27F2FDD100F1F7B9 /* AppearancePreferences.swift in Sources */, B6B1E87B26D381710062C350 /* DownloadListCoordinator.swift in Sources */, + B647EFBB2922584B00BA628D /* AdClickAttributionTabExtension.swift in Sources */, 4B980E212817604000282EE1 /* NSNotificationName+Debug.swift in Sources */, 31F7F2A6288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift in Sources */, AAC5E4F125D6BF10007F5990 /* AddressBarButton.swift in Sources */, @@ -5099,6 +5159,7 @@ AA92126F25ACCB1100600CD4 /* ErrorExtension.swift in Sources */, B6A9E47026146A250067D1B9 /* DateExtension.swift in Sources */, AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */, + B6BDD9EE29406DFA00F68088 /* WKFrameInfoExtension.swift in Sources */, B64C853D26944B940048FEBE /* PermissionStore.swift in Sources */, AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */, 4BB99D0126FE191E001E4761 /* ChromiumBookmarksReader.swift in Sources */, @@ -5201,12 +5262,14 @@ 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */, B65E6B9E26D9EC0800095F96 /* CircularProgressView.swift in Sources */, AABEE69C24A902BB0043105B /* SuggestionContainer.swift in Sources */, + B6C00ECD292F89D9009C73A6 /* FindInPageTabExtension.swift in Sources */, 85589E8327BBB8630038AD11 /* HomePageViewController.swift in Sources */, 4B59024126B35F3600489384 /* BraveDataImporter.swift in Sources */, B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */, 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */, 85AC3AEF25D5CE9800C7D2AA /* UserScripts.swift in Sources */, B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */, + B6C00ECB292F839D009C73A6 /* AutofillTabExtension.swift in Sources */, 4B677439255DBEB800025BD8 /* AppHTTPSUpgradeStore.swift in Sources */, AAB549DF25DAB8F80058460B /* BookmarkViewModel.swift in Sources */, 85707F28276A34D900DC0649 /* DaxSpeech.swift in Sources */, @@ -5235,6 +5298,7 @@ AA6FFB4624DC3B5A0028F4D0 /* WebView.swift in Sources */, B693955026F04BEB0015B914 /* ShadowView.swift in Sources */, AA3D531D27A2F58F00074EC1 /* FeedbackSender.swift in Sources */, + B6BDDA012942389000F68088 /* TabExtensions.swift in Sources */, B6CF78DE267B099C00CD4F13 /* WKNavigationActionExtension.swift in Sources */, AA7412B224D0B3AC00D22FE0 /* TabBarViewItem.swift in Sources */, 856C98D52570116900A22F1F /* NSWindow+Toast.swift in Sources */, @@ -5324,6 +5388,7 @@ 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, B63ED0DE26AFD9A300A9DAD1 /* AVCaptureDeviceMock.swift in Sources */, B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */, + B6BDD9F529409DDD00F68088 /* ContentBlockingMock.swift in Sources */, 378205FB283C277800D1D4AA /* MainMenuTests.swift in Sources */, 4B43469528655D1400177407 /* FirefoxDataImporterTests.swift in Sources */, 4B723E0926B0003E00E14D75 /* CSVLoginExporterTests.swift in Sources */, @@ -5392,6 +5457,7 @@ AA652CCE25DD9071009059CC /* BookmarkListTests.swift in Sources */, 859E7D6D274548F2009C2B69 /* BookmarksExporterTests.swift in Sources */, B6A5A2A825BAA35500AA7ADA /* WindowManagerStateRestorationTests.swift in Sources */, + B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, @@ -6245,7 +6311,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 41.1.0; + version = 41.2.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -6312,6 +6378,16 @@ package = AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + B6AE39F229374AEC00C37AA4 /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + package = B6DA44152616C13800DD1EC2 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + productName = OHHTTPStubs; + }; + B6AE39F429374AEC00C37AA4 /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + package = B6DA44152616C13800DD1EC2 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + productName = OHHTTPStubsSwift; + }; B6DA44162616C13800DD1EC2 /* OHHTTPStubs */ = { isa = XCSwiftPackageProductDependency; package = B6DA44152616C13800DD1EC2 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; diff --git a/DuckDuckGo/App Delegate/AppDelegate.swift b/DuckDuckGo/App Delegate/AppDelegate.swift index 257ef356cd..be1faa1927 100644 --- a/DuckDuckGo/App Delegate/AppDelegate.swift +++ b/DuckDuckGo/App Delegate/AppDelegate.swift @@ -24,15 +24,13 @@ import BrowserServicesKit @NSApplicationMain final class AppDelegate: NSObject, NSApplicationDelegate { - static var isRunningTests: Bool { - #if DEBUG - ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil - #else - return false - #endif - } +#if DEBUG + static var isRunningTests: Bool = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil +#else + static var isRunningTests: Bool { false } +#endif - #if DEBUG +#if DEBUG let disableCVDisplayLinkLogs: Void = { // Disable CVDisplayLink logs CFPreferencesSetValue("cv_note" as CFString, @@ -42,7 +40,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { kCFPreferencesAnyHost) CFPreferencesSynchronize("com.apple.corevideo" as CFString, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) }() - #endif +#endif let urlEventHandler = URLEventHandler() @@ -57,28 +55,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationWillFinishLaunching(_ notification: Notification) { if !Self.isRunningTests { - #if DEBUG +#if DEBUG Pixel.setUp(dryRun: true) - #else +#else Pixel.setUp() - #endif +#endif Database.shared.loadStore { _, error in guard let error = error else { return } - + switch error { case CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError): Pixel.fire(.debug(event: .dbContainerInitializationError, error: underlyingError)) default: Pixel.fire(.debug(event: .dbInitializationError, error: error)) } - + // Give Pixel a chance to be sent, but not too long Thread.sleep(forTimeInterval: 1) fatalError("Could not load DB: \(error.localizedDescription)") } } +#if DEBUG + func mock(_ className: String) -> T { + ((NSClassFromString(className) as? NSObject.Type)!.init() as? T)! + } + AppPrivacyFeatures.shared = AppDelegate.isRunningTests + // runtime mock-replacement for Unit Tests, to be redone when we‘ll be doing Dependency Injection + ? AppPrivacyFeatures(contentBlocking: mock("ContentBlockingMock"), httpsUpgradeStore: mock("HTTPSUpgradeStoreMock")) + : AppPrivacyFeatures(contentBlocking: AppContentBlocking(), httpsUpgradeStore: AppHTTPSUpgradeStore()) +#else + PrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking()) +#endif + do { let encryptionKey = Self.isRunningTests ? nil : try keyStore.readKey() fileStore = EncryptedFileStore(encryptionKey: encryptionKey) diff --git a/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift new file mode 100644 index 0000000000..32033826b2 --- /dev/null +++ b/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift @@ -0,0 +1,181 @@ +// +// AdClickAttributionTabExtension.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 os.log +import Combine +import Common +import ContentBlocking +import Foundation +import BrowserServicesKit +import PrivacyDashboard +import WebKit + +protocol AdClickAttributionDependencies { + + var privacyConfigurationManager: PrivacyConfigurationManaging { get } + var contentBlockingManager: ContentBlockerRulesManagerProtocol { get } + var tld: TLD { get } + + var adClickAttribution: AdClickAttributing { get } + var adClickAttributionRulesProvider: AdClickAttributionRulesProviding { get } + + var attributionEvents: EventMapping? { get } + var attributionDebugEvents: EventMapping? { get } + +} + +protocol UserContentControllerProtocol: AnyObject { + func enableGlobalContentRuleList(withIdentifier identifier: String) throws + func disableGlobalContentRuleList(withIdentifier identifier: String) throws + func removeLocalContentRuleList(withIdentifier identifier: String) + func installLocalContentRuleList(_ ruleList: WKContentRuleList, identifier: String) +} +extension UserContentController: UserContentControllerProtocol {} +typealias UserContentControllerProvider = () -> UserContentControllerProtocol? + +final class AdClickAttributionTabExtension: TabExtension { + + private static func makeAdClickAttributionDetection(with dependencies: some AdClickAttributionDependencies) -> AdClickAttributionDetection { + return AdClickAttributionDetection(feature: dependencies.adClickAttribution, + tld: dependencies.tld, + eventReporting: dependencies.attributionEvents, + errorReporting: dependencies.attributionDebugEvents, + log: OSLog.attribution) + + } + + private static func makeAdClickAttributionLogic(with dependencies: some AdClickAttributionDependencies) -> AdClickAttributionLogic { + return AdClickAttributionLogic(featureConfig: dependencies.adClickAttribution, + rulesProvider: dependencies.adClickAttributionRulesProvider, + tld: dependencies.tld, + eventReporting: dependencies.attributionEvents, + errorReporting: dependencies.attributionDebugEvents, + log: OSLog.attribution) + } + + private let dependencies: any AdClickAttributionDependencies + + private let userContentControllerProvider: UserContentControllerProvider + private weak var contentBlockerRulesScript: ContentBlockerRulesUserScript? + private var cancellables = Set() + + private(set) var detection: AdClickAttributionDetection! + private(set) var logic: AdClickAttributionLogic! + + public var currentAttributionState: AdClickAttributionLogic.State? { + logic.state + } + + init(inheritedAttribution: AdClickAttributionLogic.State?, + userContentControllerProvider: @escaping UserContentControllerProvider, + contentBlockerRulesScriptPublisher: some Publisher, + trackerInfoPublisher: some Publisher, + dependencies: some AdClickAttributionDependencies) { + + self.dependencies = dependencies + self.userContentControllerProvider = userContentControllerProvider + + self.detection = Self.makeAdClickAttributionDetection(with: dependencies) + self.logic = Self.makeAdClickAttributionLogic(with: dependencies) + + logic.delegate = self + detection.delegate = logic + + if let state = inheritedAttribution { + logic.applyInheritedAttribution(state: state) + } + + contentBlockerRulesScriptPublisher + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] contentBlockerRulesScript in + guard let self else { return } + self.contentBlockerRulesScript = contentBlockerRulesScript + self.logic.onRulesChanged(latestRules: self.dependencies.contentBlockingManager.currentRules) + } + .store(in: &cancellables) + + trackerInfoPublisher + .sink { [weak self] tracker in + self?.logic.onRequestDetected(request: tracker) + } + .store(in: &cancellables) + } + +} + +extension AdClickAttributionTabExtension: AdClickAttributionLogicDelegate { + + func attributionLogic(_ logic: AdClickAttributionLogic, + didRequestRuleApplication rules: ContentBlockerRulesManager.Rules?, + forVendor vendor: String?) { + guard let userContentController = userContentControllerProvider(), + let contentBlockerRulesScript + else { + assertionFailure("UserScripts not loaded") + return + } + + let attributedTempListName = AdClickAttributionRulesProvider.Constants.attributedTempRuleListName + + guard dependencies.privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .contentBlocking) + else { + userContentController.removeLocalContentRuleList(withIdentifier: attributedTempListName) + contentBlockerRulesScript.currentAdClickAttributionVendor = nil + contentBlockerRulesScript.supplementaryTrackerData = [] + return + } + + contentBlockerRulesScript.currentAdClickAttributionVendor = vendor + if let rules = rules { + + let globalListName = DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName + let globalAttributionListName = AdClickAttributionRulesSplitter.blockingAttributionRuleListName(forListNamed: globalListName) + + if vendor != nil { + userContentController.installLocalContentRuleList(rules.rulesList, identifier: attributedTempListName) + try? userContentController.disableGlobalContentRuleList(withIdentifier: globalAttributionListName) + } else { + userContentController.removeLocalContentRuleList(withIdentifier: attributedTempListName) + try? userContentController.enableGlobalContentRuleList(withIdentifier: globalAttributionListName) + } + + contentBlockerRulesScript.supplementaryTrackerData = [rules.trackerData] + } else { + contentBlockerRulesScript.supplementaryTrackerData = [] + } + } + +} + +extension AppContentBlocking: AdClickAttributionDependencies {} + +protocol AdClickAttributionProtocol { + var detection: AdClickAttributionDetection! { get } + var logic: AdClickAttributionLogic! { get } +} + +extension AdClickAttributionTabExtension: AdClickAttributionProtocol { + func getPublicProtocol() -> AdClickAttributionProtocol { self } +} + +extension TabExtensions { + var adClickAttribution: AdClickAttributionProtocol? { + resolve(AdClickAttributionTabExtension.self) + } +} diff --git a/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift new file mode 100644 index 0000000000..e37f5c9a7c --- /dev/null +++ b/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift @@ -0,0 +1,161 @@ +// +// AutofillTabExtension.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 BrowserServicesKit +import Combine +import Foundation + +final class AutofillTabExtension: TabExtension { + + static var emailManagerProvider: (EmailManagerRequestDelegate) -> AutofillEmailDelegate = { delegate in + let emailManager = EmailManager() + emailManager.requestDelegate = delegate + return emailManager + } + + static var vaultManagerProvider: (SecureVaultManagerDelegate) -> AutofillSecureVaultDelegate = { delegate in + let manager = SecureVaultManager() + manager.delegate = delegate + return manager + } + + private weak var delegate: ContentOverlayUserScriptDelegate? + + func setDelegate(_ delegate: ContentOverlayUserScriptDelegate?) { + self.delegate = delegate + autofillScript?.currentOverlayTab = delegate + } + + private var autofillUserScriptCancellable: AnyCancellable? + + private weak var autofillScript: WebsiteAutofillUserScript? { + didSet { + autofillScript?.currentOverlayTab = self.delegate + } + } + private var emailManager: AutofillEmailDelegate? + private var vaultManager: AutofillSecureVaultDelegate? + + @Published var autofillDataToSave: AutofillData? + + init(autofillUserScriptPublisher: some Publisher) { + autofillUserScriptCancellable = autofillUserScriptPublisher.sink { [weak self] autofillScript in + guard let self, let autofillScript else { return } + + self.autofillScript = autofillScript + self.emailManager = Self.emailManagerProvider(self) + autofillScript.emailDelegate = self.emailManager + self.vaultManager = Self.vaultManagerProvider(self) + autofillScript.vaultDelegate = self.vaultManager + } + } + + func didClick(at point: CGPoint) { + autofillScript?.clickPoint = point + } + +} + +extension AutofillTabExtension: SecureVaultManagerDelegate { + + public func secureVaultManagerIsEnabledStatus(_: SecureVaultManager) -> Bool { + return true + } + + func secureVaultManager(_: SecureVaultManager, promptUserToStoreAutofillData data: AutofillData) { + self.autofillDataToSave = data + } + + func secureVaultManager(_: SecureVaultManager, + promptUserToAutofillCredentialsForDomain domain: String, + withAccounts accounts: [SecureVaultModels.WebsiteAccount], + withTrigger trigger: AutofillUserScript.GetTriggerType, + completionHandler: @escaping (SecureVaultModels.WebsiteAccount?) -> Void) { + // no-op on macOS + } + + func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: String) { + Pixel.fire(.formAutofilled(kind: type.formAutofillKind)) + } + + func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler handler: @escaping (Bool) -> Void) { + DeviceAuthenticator.shared.authenticateUser(reason: .autofill) { authenticationResult in + handler(authenticationResult.authenticated) + } + } + + func secureVaultInitFailed(_ error: SecureVaultError) { + SecureVaultErrorReporter.shared.secureVaultInitFailed(error) + } + + public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { + Pixel.fire(.jsPixel(pixel)) + } + + func secureVaultManagerShouldAutomaticallyUpdateCredentialsWithoutUsername(_: SecureVaultManager) -> Bool { + return true + } + +} + +extension AutofillType { + var formAutofillKind: Pixel.Event.FormAutofillKind { + switch self { + case .password: return .password + case .card: return .card + case .identity: return .identity + } + } +} + +extension AutofillTabExtension: EmailManagerRequestDelegate { } + +protocol AutofillProtocol { + func setDelegate(_: ContentOverlayUserScriptDelegate?) + func didClick(at point: CGPoint) + + var autofillDataToSavePublisher: AnyPublisher { get } + func resetAutofillData() +} + +extension AutofillTabExtension: AutofillProtocol { + func getPublicProtocol() -> AutofillProtocol { self } + + var autofillDataToSavePublisher: AnyPublisher { + self.$autofillDataToSave.eraseToAnyPublisher() + } + func resetAutofillData() { + self.autofillDataToSave = nil + } +} + +extension TabExtensions { + var autofill: AutofillProtocol? { resolve(AutofillTabExtension.self) } +} + +extension Tab { + + var autofillDataToSavePublisher: AnyPublisher { + self.autofill?.autofillDataToSavePublisher.eraseToAnyPublisher() ?? Just(nil).eraseToAnyPublisher() + } + + func resetAutofillData() { + self.autofill?.resetAutofillData() + } + +} diff --git a/DuckDuckGo/Browser Tab/Model/ContextMenuManager.swift b/DuckDuckGo/Browser Tab/Extensions/ContextMenuManager.swift similarity index 93% rename from DuckDuckGo/Browser Tab/Model/ContextMenuManager.swift rename to DuckDuckGo/Browser Tab/Extensions/ContextMenuManager.swift index 8be859a892..25c368a35e 100644 --- a/DuckDuckGo/Browser Tab/Model/ContextMenuManager.swift +++ b/DuckDuckGo/Browser Tab/Extensions/ContextMenuManager.swift @@ -17,27 +17,31 @@ // import AppKit +import Combine import Foundation import WebKit -protocol ContextMenuManagerDelegate: AnyObject { - func launchSearch(for text: String) - func prepareForContextMenuDownload() -} - enum NavigationDecision { case allow(NewWindowPolicy) case cancel } final class ContextMenuManager: NSObject { - - weak var delegate: ContextMenuManagerDelegate? + private var userScriptCancellable: AnyCancellable? private var onNewWindow: ((WKNavigationAction?) -> NavigationDecision)? private var askForDownloadLocation: Bool? private var originalItems: [WKMenuItemIdentifier: NSMenuItem]? private var selectedText: String? + fileprivate weak var webView: WKWebView? + + init(contextMenuScriptPublisher: some Publisher) { + super.init() + + userScriptCancellable = contextMenuScriptPublisher.sink { [weak self] contextMenuScript in + contextMenuScript?.delegate = self + } + } func decideNewWindowPolicy(for navigationAction: WKNavigationAction) -> NavigationDecision? { defer { @@ -144,6 +148,8 @@ extension ContextMenuManager: WebViewContextMenuDelegate { guard let identifier = item.identifier.flatMap(WKMenuItemIdentifier.init) else { continue } Self.menuItemHandlers[identifier]?(self)(item, index, menu) } + + self.webView = webView } func webView(_ webView: WebView, didCloseContextMenu menu: NSMenu, with event: NSEvent?) { @@ -236,12 +242,18 @@ private extension ContextMenuManager { @objc extension ContextMenuManager { func search(_ sender: NSMenuItem) { - guard let selectedText = selectedText else { + guard let selectedText, + let url = URL.makeSearchUrl(from: selectedText), + let webView + else { assertionFailure("Failed to get search term") return } - delegate?.launchSearch(for: selectedText) + self.onNewWindow = { _ in + .allow(.tab(selected: true)) + } + webView.loadInNewWindow(url) } func openLinkInNewTab(_ sender: NSMenuItem) { @@ -296,7 +308,6 @@ private extension ContextMenuManager { return } - delegate?.prepareForContextMenuDownload() askForDownloadLocation = true NSApp.sendAction(action, to: originalItem.target, from: originalItem) } @@ -383,7 +394,6 @@ private extension ContextMenuManager { return } - delegate?.prepareForContextMenuDownload() askForDownloadLocation = true NSApp.sendAction(action, to: originalItem.target, from: originalItem) } @@ -413,9 +423,25 @@ private extension ContextMenuManager { // MARK: - ContextMenuUserScriptDelegate extension ContextMenuManager: ContextMenuUserScriptDelegate { - func willShowContextMenu(withSelectedText selectedText: String) { self.selectedText = selectedText } } + +// MARK: - TabExtensions + +protocol ContextMenuManagerProtocol: WebViewContextMenuDelegate { + func decideNewWindowPolicy(for navigationAction: WKNavigationAction) -> NavigationDecision? + func shouldAskForDownloadLocation() -> Bool? +} + +extension ContextMenuManager: TabExtension, ContextMenuManagerProtocol { + func getPublicProtocol() -> ContextMenuManagerProtocol { self } +} + +extension TabExtensions { + var contextMenuManager: ContextMenuManagerProtocol? { + resolve(ContextMenuManager.self) + } +} diff --git a/DuckDuckGo/Browser Tab/Extensions/FindInPageTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/FindInPageTabExtension.swift new file mode 100644 index 0000000000..76eaf8efdd --- /dev/null +++ b/DuckDuckGo/Browser Tab/Extensions/FindInPageTabExtension.swift @@ -0,0 +1,75 @@ +// +// FindInPageTabExtension.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 Combine +import Foundation + +final class FindInPageTabExtension: TabExtension { + + let model: FindInPageModel + private let userScriptCancellable: AnyCancellable? + + var isVisible: Bool = false + + init(findInPageScriptPublisher: some Publisher) { + model = FindInPageModel() + userScriptCancellable = findInPageScriptPublisher.sink { [weak model] findInPageScript in + findInPageScript?.model = model + } + } + + func show(with webView: WKWebView) { + model.show(with: webView) + if !model.text.isEmpty { + model.find(model.text) + } + } + + func close() { + guard model.isVisible else { return } + model.findDone() + model.close() + } + + func findNext() { + model.findNext() + } + + func findPrevious() { + model.findPrevious() + } + +} + +protocol FindInPageProtocol { + var model: FindInPageModel { get } + func show(with webView: WKWebView) + func close() + func findNext() + func findPrevious() +} + +extension FindInPageTabExtension: FindInPageProtocol { + func getPublicProtocol() -> FindInPageProtocol { self } +} + +extension TabExtensions { + var findInPage: FindInPageProtocol? { + resolve(FindInPageTabExtension.self) + } +} diff --git a/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift new file mode 100644 index 0000000000..d24a8c2306 --- /dev/null +++ b/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift @@ -0,0 +1,63 @@ +// +// HoveredLinkTabExtension.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 Combine +import Foundation + +final class HoveredLinkTabExtension: TabExtension { + + private var cancellable: AnyCancellable? + fileprivate var hoveredLinkSubject = PassthroughSubject() + + init(hoverUserScriptPublisher: some Publisher) { + cancellable = hoverUserScriptPublisher.sink { [weak self] hoverUserScript in + hoverUserScript?.delegate = self + } + } + +} + +extension HoveredLinkTabExtension: HoverUserScriptDelegate { + func hoverUserScript(_ script: HoverUserScript, didChange url: URL?) { + hoveredLinkSubject.send(url) + } +} + +protocol HoveredLinksProtocol { + var hoveredLinkPublisher: AnyPublisher { get } +} + +extension HoveredLinkTabExtension: HoveredLinksProtocol { + func getPublicProtocol() -> HoveredLinksProtocol { self } + + var hoveredLinkPublisher: AnyPublisher { + hoveredLinkSubject.eraseToAnyPublisher() + } +} + +extension TabExtensions { + var hoveredLinks: HoveredLinksProtocol? { + resolve(HoveredLinkTabExtension.self) + } +} + +extension Tab { + var hoveredLinkPublisher: AnyPublisher { + self.hoveredLinks?.hoveredLinkPublisher ?? Just(nil).eraseToAnyPublisher() + } +} diff --git a/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift b/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift new file mode 100644 index 0000000000..a2df797b10 --- /dev/null +++ b/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift @@ -0,0 +1,126 @@ +// +// TabExtensions.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 BrowserServicesKit +import Combine +import ContentBlocking +import Foundation +import PrivacyDashboard + +/** + Tab Extensions should conform to TabExtension protocol + To access an extension from other places you need to define its Public Protocol and extend `TabExtensions` using `resolve(ExtensionClass.self)` to get the extension: +``` + class MyTabExtension { + fileprivate var featureModel: FeatureModel + } + + protocol MyExtensionPublicProtocol { + var publicVar { get } + } + + extension MyTabExtension: TabExtension, MyExtensionPublicProtocol { + func getPublicProtocol() -> MyExtensionPublicProtocol { self } + } + + extension TabExtensions { + var myFeature: MyExtensionPublicProtocol? { + extensions.resolve(MyTabExtension.self) + } + } + ``` + **/ +protocol TabExtension { + associatedtype PublicProtocol + func getPublicProtocol() -> PublicProtocol +} + +// Implement these methods for Extension State Restoration +protocol NSCodingExtension: TabExtension { + func encode(using coder: NSCoder) + func awakeAfter(using decoder: NSCoder) +} + +// Define dependencies used to instantiate TabExtensions here: +protocol TabExtensionDependencies { + var userScriptsPublisher: AnyPublisher { get } + var contentBlocking: ContentBlockingProtocol { get } + var adClickAttributionDependencies: AdClickAttributionDependencies { get } + var privacyInfoPublisher: AnyPublisher { get } + + var inheritedAttribution: AdClickAttributionLogic.State? { get } + var userContentControllerProvider: UserContentControllerProvider { get } +} + +extension AppTabExtensions { + + /// Instantiate `TabExtension`-s for App builds here + /// use add { return SomeTabExtensions() } to register Tab Extensions + /// assign a result of add { .. } to a variable to use the registered Extensions for providing dependencies to other extensions + /// ` add { MySimpleExtension() } + /// ` let myPublishingExtension = add { MyPublishingExtension() } + /// ` add { MyOtherExtension(with: myExtension.resultPublisher) } + /// Note: Extensions with state restoration support should conform to `NSCodingExtension` + mutating func make(with dependencies: TabExtensionDependencies) { + let userScripts = dependencies.userScriptsPublisher + + let trackerInfoPublisher = dependencies.privacyInfoPublisher + .compactMap { $0?.$trackerInfo } + .switchToLatest() + .scan( (old: Set(), new: Set()) ) { + ($0.new, $1.trackers) + } + .map { (old, new) in + new.subtracting(old).publisher + } + .switchToLatest() + + add { + AdClickAttributionTabExtension(inheritedAttribution: dependencies.inheritedAttribution, + userContentControllerProvider: dependencies.userContentControllerProvider, + contentBlockerRulesScriptPublisher: userScripts.map(\.?.contentBlockerRulesScript), + trackerInfoPublisher: trackerInfoPublisher, + dependencies: dependencies.adClickAttributionDependencies) + } + add { + AutofillTabExtension(autofillUserScriptPublisher: userScripts.map(\.?.autofillScript)) + } + add { + ContextMenuManager(contextMenuScriptPublisher: userScripts.map(\.?.contextMenuScript)) + } + add { + HoveredLinkTabExtension(hoverUserScriptPublisher: userScripts.map(\.?.hoverUserScript)) + } + add { + FindInPageTabExtension(findInPageScriptPublisher: userScripts.map(\.?.findInPageScript)) + } + } + +} + +#if DEBUG +extension TestTabExtensions { + + /// Add `TabExtension`-s that should be loaded when running Unit Tests here + /// By default the Extensions won‘t be loaded + mutating func make(with dependencies: TabExtensionDependencies) { + + } + +} +#endif diff --git a/DuckDuckGo/Browser Tab/Model/NewWindowPolicy.swift b/DuckDuckGo/Browser Tab/Model/NewWindowPolicy.swift index e6838f2e85..afe014f97f 100644 --- a/DuckDuckGo/Browser Tab/Model/NewWindowPolicy.swift +++ b/DuckDuckGo/Browser Tab/Model/NewWindowPolicy.swift @@ -32,6 +32,15 @@ enum NewWindowPolicy { } } + var isTab: Bool { + if case .tab = self { return true } + return false + } + var isSelectedTab: Bool { + if case .tab(selected: true) = self { return true } + return false + } + } extension WKWindowFeatures { diff --git a/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift index c9746672b2..458ef6fad2 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift @@ -20,7 +20,12 @@ import Combine import Foundation import WebKit -extension Tab: WKUIDelegate { +extension Tab: WKUIDelegate, PrintingUserScriptDelegate { + + // "protected" delegate property + private var delegate: TabDelegate? { + self.value(forKeyPath: Tab.objcDelegateKeyPath) as? TabDelegate + } @objc(_webView:saveDataToFile:suggestedFilename:mimeType:originatingURL:) func webView(_ webView: WKWebView, saveDataToFile data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) { @@ -65,8 +70,8 @@ extension Tab: WKUIDelegate { completionHandler: @escaping (WKWebView?) -> Void) { let newWindowPolicy: NavigationDecision? = { - // Are we handling custom Context Menu actions (see ContextMenuManager?) - if let newWindowPolicy = contextMenuManager.decideNewWindowPolicy(for: navigationAction) { + // Are we handling custom Context Menu navigation action? (see ContextMenuManager) + if let newWindowPolicy = self.contextMenuManager?.decideNewWindowPolicy(for: navigationAction) { return newWindowPolicy } @@ -115,7 +120,7 @@ extension Tab: WKUIDelegate { guard let delegate else { return nil } - let tab = Tab(content: .none, webViewConfiguration: configuration, parentTab: self, shouldLoadInBackground: false, webViewFrame: webView.superview?.bounds ?? .zero) + let tab = Tab(content: .none, webViewConfiguration: configuration, parentTab: self, shouldLoadInBackground: false, canBeClosedWithBack: kind.isSelectedTab, webViewFrame: webView.superview?.bounds ?? .zero) delegate.tab(self, createdChild: tab, of: kind) let webView = tab.webView @@ -250,8 +255,8 @@ extension Tab: WKUIDelegate { delegate?.closeTab(self) } - func print(frame: Any? = nil, completionHandler: ((Bool) -> Void)? = nil) { - guard let printOperation = webView.printOperation(for: frame) else { return } + func runPrintOperation(for frameHandle: Any?, in webView: WKWebView, completionHandler: ((Bool) -> Void)? = nil) { + guard let printOperation = webView.printOperation(for: frameHandle) else { return } if printOperation.view?.frame.isEmpty == true { printOperation.view?.frame = webView.bounds @@ -264,15 +269,17 @@ extension Tab: WKUIDelegate { } @objc(_webView:printFrame:) - func webView(_ webView: WKWebView, printFrame handle: Any) { - self.print(frame: handle) + func webView(_ webView: WKWebView, printFrame frameHandle: Any) { + self.runPrintOperation(for: frameHandle, in: webView) } - @available(macOS 12, *) @objc(_webView:printFrame:pdfFirstPageSize:completionHandler:) - func webView(_ webView: WKWebView, printFrame handle: Any, pdfFirstPageSize size: CGSize, completionHandler: () -> Void) { - self.webView(webView, printFrame: handle) - completionHandler() + func webView(_ webView: WKWebView, printFrame frameHandle: Any, pdfFirstPageSize size: CGSize, completionHandler: @escaping () -> Void) { + self.runPrintOperation(for: frameHandle, in: webView) { _ in completionHandler() } + } + + func print() { + self.runPrintOperation(for: nil, in: self.webView) } } diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index b0f3b0e7b9..d27a5c7fd1 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -34,9 +34,7 @@ protocol TabDelegate: ContentOverlayUserScriptDelegate { func tab(_ tab: Tab, createdChild childTab: Tab, of kind: NewWindowPolicy) func tab(_ tab: Tab, requestedOpenExternalURL url: URL, forUserEnteredURL userEntered: Bool) -> Bool - func tab(_ tab: Tab, requestedSaveAutofillData autofillData: AutofillData) func tab(_ tab: Tab, promptUserForCookieConsent result: @escaping (Bool) -> Void) - func tab(_ tab: Tab, didChangeHoverLink url: URL?) func tabPageDOMLoaded(_ tab: Tab) func closeTab(_ tab: Tab) @@ -45,7 +43,8 @@ protocol TabDelegate: ContentOverlayUserScriptDelegate { } -// swiftlint:disable:next type_body_length +// swiftlint:disable type_body_length +@dynamicMemberLookup final class Tab: NSObject, Identifiable, ObservableObject { enum TabContent: Equatable { @@ -147,51 +146,115 @@ final class Tab: NSObject, Identifiable, ObservableObject { } } } - - let contextMenuManager = ContextMenuManager() - private weak var autofillScript: WebsiteAutofillUserScript? - weak var delegate: TabDelegate? { - didSet { - autofillScript?.currentOverlayTab = delegate - contextMenuManager.delegate = self - } - } - - var isPinned: Bool { - return pinnedTabsManager.isTabPinned(self) + private struct ExtensionDependencies: TabExtensionDependencies { + var userScriptsPublisher: AnyPublisher + var contentBlocking: ContentBlockingProtocol + var adClickAttributionDependencies: AdClickAttributionDependencies + var privacyInfoPublisher: AnyPublisher + var inheritedAttribution: BrowserServicesKit.AdClickAttributionLogic.State? + var userContentControllerProvider: UserContentControllerProvider } - + + // "protected" delegate property for extensions usage + private weak var delegate: TabDelegate? + @objc private var objcDelegate: Any? { delegate } + static var objcDelegateKeyPath: String { #keyPath(objcDelegate) } + func setDelegate(_ delegate: TabDelegate) { self.delegate = delegate } + private let cbaTimeReporter: ContentBlockingAssetsCompilationTimeReporter? - private let pinnedTabsManager: PinnedTabsManager + let pinnedTabsManager: PinnedTabsManager private let privatePlayer: PrivatePlayer + private let privacyFeatures: AnyPrivacyFeatures + private var contentBlocking: AnyContentBlocking { privacyFeatures.contentBlocking } + + private let webViewConfiguration: WKWebViewConfiguration + + private var extensions: TabExtensions + // accesing TabExtensions‘ Public Protocols projecting tab.extensions.extensionName to tab.extensionName + // allows extending Tab functionality while maintaining encapsulation + subscript(dynamicMember keyPath: KeyPath) -> Extension? { + self.extensions[keyPath: keyPath] + } + + @Published + private(set) var userContentController: UserContentController? + + convenience init(content: TabContent, + faviconManagement: FaviconManagement = FaviconManager.shared, + webCacheManager: WebCacheManager = WebCacheManager.shared, + webViewConfiguration: WKWebViewConfiguration? = nil, + historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, + pinnedTabsManager: PinnedTabsManager = WindowControllersManager.shared.pinnedTabsManager, + privatePlayer: PrivatePlayer? = nil, + cbaTimeReporter: ContentBlockingAssetsCompilationTimeReporter? = ContentBlockingAssetsCompilationTimeReporter.shared, + localHistory: Set = Set(), + title: String? = nil, + error: Error? = nil, + favicon: NSImage? = nil, + sessionStateData: Data? = nil, + interactionStateData: Data? = nil, + parentTab: Tab? = nil, + shouldLoadInBackground: Bool = false, + canBeClosedWithBack: Bool = false, + lastSelectedAt: Date? = nil, + currentDownload: URL? = nil, + webViewFrame: CGRect = .zero + ) { + + let privatePlayer = privatePlayer + ?? (AppDelegate.isRunningTests ? PrivatePlayer.mock(withMode: .enabled) : PrivatePlayer.shared) + + self.init(content: content, + faviconManagement: faviconManagement, + webCacheManager: webCacheManager, + webViewConfiguration: webViewConfiguration, + historyCoordinating: historyCoordinating, + pinnedTabsManager: pinnedTabsManager, + privacyFeatures: PrivacyFeatures, + privatePlayer: privatePlayer, + cbaTimeReporter: cbaTimeReporter, + localHistory: localHistory, + title: title, + error: error, + favicon: favicon, + sessionStateData: sessionStateData, + interactionStateData: interactionStateData, + parentTab: parentTab, + shouldLoadInBackground: shouldLoadInBackground, + canBeClosedWithBack: canBeClosedWithBack, + lastSelectedAt: lastSelectedAt, + currentDownload: currentDownload, + webViewFrame: webViewFrame) + } init(content: TabContent, - faviconManagement: FaviconManagement = FaviconManager.shared, - webCacheManager: WebCacheManager = WebCacheManager.shared, - webViewConfiguration: WKWebViewConfiguration? = nil, - historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, - pinnedTabsManager: PinnedTabsManager = WindowControllersManager.shared.pinnedTabsManager, - privatePlayer: PrivatePlayer = .shared, - cbaTimeReporter: ContentBlockingAssetsCompilationTimeReporter? = ContentBlockingAssetsCompilationTimeReporter.shared, - localHistory: Set = Set(), - title: String? = nil, - error: Error? = nil, - favicon: NSImage? = nil, - sessionStateData: Data? = nil, - interactionStateData: Data? = nil, - parentTab: Tab? = nil, - attributionState: AdClickAttributionLogic.State? = nil, + faviconManagement: FaviconManagement, + webCacheManager: WebCacheManager, + webViewConfiguration: WKWebViewConfiguration?, + historyCoordinating: HistoryCoordinating, + pinnedTabsManager: PinnedTabsManager, + privacyFeatures: some PrivacyFeaturesProtocol, + privatePlayer: PrivatePlayer, + cbaTimeReporter: ContentBlockingAssetsCompilationTimeReporter?, + localHistory: Set, + title: String?, + error: Error?, + favicon: NSImage?, + sessionStateData: Data?, + interactionStateData: Data?, + parentTab: Tab?, shouldLoadInBackground: Bool, - canBeClosedWithBack: Bool = false, - lastSelectedAt: Date? = nil, - currentDownload: URL? = nil, - webViewFrame: CGRect = .zero + canBeClosedWithBack: Bool, + lastSelectedAt: Date?, + currentDownload: URL?, + webViewFrame: CGRect ) { self.content = content self.faviconManagement = faviconManagement self.historyCoordinating = historyCoordinating self.pinnedTabsManager = pinnedTabsManager + self.privacyFeatures = privacyFeatures self.privatePlayer = privatePlayer self.cbaTimeReporter = cbaTimeReporter self.localHistory = localHistory @@ -206,16 +269,35 @@ final class Tab: NSObject, Identifiable, ObservableObject { self.currentDownload = currentDownload let configuration = webViewConfiguration ?? WKWebViewConfiguration() - configuration.applyStandardConfiguration() - + configuration.applyStandardConfiguration(contentBlocking: privacyFeatures.contentBlocking) + self.webViewConfiguration = configuration + let userContentController = configuration.userContentController as? UserContentController + assert(userContentController != nil) + self.userContentController = userContentController + webView = WebView(frame: webViewFrame, configuration: configuration) webView.allowsLinkPreview = false permissions = PermissionModel() + let userScriptsPublisher = _userContentController.projectedValue + .compactMap { $0?.$contentBlockingAssets } + .switchToLatest() + .map { $0?.userScripts as? UserScripts } + .eraseToAnyPublisher() + + var userContentControllerProvider: UserContentControllerProvider? + self.extensions = .builder().build(with: ExtensionDependencies(userScriptsPublisher: userScriptsPublisher, + contentBlocking: privacyFeatures.contentBlocking, + adClickAttributionDependencies: privacyFeatures.contentBlocking, + privacyInfoPublisher: _privacyInfo.projectedValue.eraseToAnyPublisher(), + userContentControllerProvider: { userContentControllerProvider?() })) + super.init() + userContentControllerProvider = { [weak self] in self?.userContentController } - initAttributionLogic(state: attributionState ?? parentTab?.adClickAttributionLogic.state) + userContentController?.delegate = self setupWebView(shouldLoadInBackground: shouldLoadInBackground) + if favicon == nil { handleFavicon() } @@ -226,6 +308,28 @@ final class Tab: NSObject, Identifiable, ObservableObject { object: nil) } + override func awakeAfter(using decoder: NSCoder) -> Any? { + for tabExtension in self.extensions { + (tabExtension as? (any NSCodingExtension))?.awakeAfter(using: decoder) + } + return self + } + + func encodeExtensions(with coder: NSCoder) { + for tabExtension in self.extensions { + (tabExtension as? (any NSCodingExtension))?.encode(using: coder) + } + } + + func openChild(with content: TabContent, of kind: NewWindowPolicy) { + guard let delegate else { + assertionFailure("no delegate set") + return + } + let tab = Tab(content: content, parentTab: self, shouldLoadInBackground: true, canBeClosedWithBack: kind.isSelectedTab) + delegate.tab(self, createdChild: tab, of: kind) + } + @objc func onDuckDuckGoEmailSignOut(_ notification: Notification) { guard let url = webView.url else { return } if EmailUrls().isDuckDuckGoEmailProtection(url: url) { @@ -250,10 +354,6 @@ final class Tab: NSObject, Identifiable, ObservableObject { cbaTimeReporter?.tabWillClose(self.instrumentation.currentTabIdentifier) } - private var userContentController: UserContentController? { - webView.configuration.userContentController as? UserContentController - } - // MARK: - Event Publishers let webViewDidReceiveChallengePublisher = PassthroughSubject() @@ -349,12 +449,6 @@ final class Tab: NSObject, Identifiable, ObservableObject { return _canBeClosedWithBack } - weak var findInPage: FindInPageModel? { - didSet { - attachFindInPage() - } - } - @available(macOS, obsoleted: 12.0, renamed: "interactionStateData") var sessionStateData: Data? var interactionStateData: Data? @@ -542,34 +636,16 @@ final class Tab: NSObject, Identifiable, ObservableObject { } lazy var linkProtection: LinkProtection = { - LinkProtection(privacyManager: ContentBlocking.shared.privacyConfigurationManager, - contentBlockingManager: ContentBlocking.shared.contentBlockingManager, + LinkProtection(privacyManager: contentBlocking.privacyConfigurationManager, + contentBlockingManager: contentBlocking.contentBlockingManager, errorReporting: Self.debugEvents) }() - + lazy var referrerTrimming: ReferrerTrimming = { - ReferrerTrimming(privacyManager: ContentBlocking.shared.privacyConfigurationManager, - contentBlockingManager: ContentBlocking.shared.contentBlockingManager, - tld: ContentBlocking.shared.tld) + ReferrerTrimming(privacyManager: contentBlocking.privacyConfigurationManager, + contentBlockingManager: contentBlocking.contentBlockingManager, + tld: contentBlocking.tld) }() - - // MARK: - Ad Click Attribution - - private let adClickAttributionDetection = ContentBlocking.shared.makeAdClickAttributionDetection() - let adClickAttributionLogic = ContentBlocking.shared.makeAdClickAttributionLogic() - - public var currentAttributionState: AdClickAttributionLogic.State? { - adClickAttributionLogic.state - } - - private func initAttributionLogic(state: AdClickAttributionLogic.State?) { - adClickAttributionLogic.delegate = self - adClickAttributionDetection.delegate = adClickAttributionLogic - - if let state = state { - adClickAttributionLogic.applyInheritedAttribution(state: state) - } - } @MainActor private func reloadIfNeeded(shouldLoadInBackground: Bool = false) async { @@ -712,10 +788,10 @@ final class Tab: NSObject, Identifiable, ObservableObject { private func setupWebView(shouldLoadInBackground: Bool) { webView.navigationDelegate = self webView.uiDelegate = self - webView.contextMenuDelegate = contextMenuManager + webView.contextMenuDelegate = self.contextMenuManager webView.allowsBackForwardNavigationGestures = true webView.allowsMagnification = true - userContentController?.delegate = self + permissions.webView = webView superviewObserver = webView.observe(\.superview, options: .old) { [weak self] _, change in @@ -763,38 +839,6 @@ final class Tab: NSObject, Identifiable, ObservableObject { } } - // MARK: - User Scripts - - lazy var emailManager: EmailManager = { - let emailManager = EmailManager() - emailManager.requestDelegate = self - return emailManager - }() - - lazy var vaultManager: SecureVaultManager = { - let manager = SecureVaultManager(passwordManager: PasswordManagerCoordinator.shared) - manager.delegate = self - return manager - }() - - // MARK: - Find in Page - - weak var findInPageScript: FindInPageUserScript? - var findInPageCancellable: AnyCancellable? - private func subscribeToFindInPageTextChange() { - findInPageCancellable?.cancel() - if let findInPage = findInPage { - findInPageCancellable = findInPage.$text.receive(on: DispatchQueue.main).sink { [weak self] text in - self?.find(text: text) - } - } - } - - private func attachFindInPage() { - findInPageScript?.model = findInPage - subscribeToFindInPageTextChange() - } - // MARK: - Global & Local History private var historyCoordinating: HistoryCoordinating @@ -827,7 +871,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { private var youtubePlayerCancellables: Set = [] func setUpYoutubeScriptsIfNeeded() { - guard PrivatePlayer.shared.isAvailable else { + guard privatePlayer.isAvailable else { return } @@ -898,7 +942,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { private func makePrivacyInfo(url: URL) -> PrivacyInfo? { guard let host = url.host else { return nil } - let entity = ContentBlocking.shared.trackerDataManager.trackerData.findEntity(forHost: host) + let entity = contentBlocking.trackerDataManager.trackerData.findEntity(forHost: host) privacyInfo = PrivacyInfo(url: url, parentEntity: entity, @@ -926,7 +970,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { } private func makeProtectionStatus(for host: String) -> ProtectionStatus { - let config = ContentBlocking.shared.privacyConfigurationManager.privacyConfig + let config = contentBlocking.privacyConfigurationManager.privacyConfig let isTempUnprotected = config.isTempUnprotected(domain: host) let isAllowlisted = config.isUserUnprotected(domain: host) @@ -952,17 +996,11 @@ extension Tab: UserContentControllerDelegate { userScripts.debugScript.instrumentation = instrumentation userScripts.faviconScript.delegate = self - userScripts.contextMenuScript.delegate = self.contextMenuManager userScripts.surrogatesScript.delegate = self userScripts.contentBlockerRulesScript.delegate = self userScripts.clickToLoadScript.delegate = self - userScripts.autofillScript.currentOverlayTab = self.delegate - userScripts.autofillScript.emailDelegate = emailManager - userScripts.autofillScript.vaultDelegate = vaultManager - self.autofillScript = userScripts.autofillScript userScripts.pageObserverScript.delegate = self userScripts.printingUserScript.delegate = self - userScripts.hoverUserScript.delegate = self if #available(macOS 11, *) { userScripts.autoconsentUserScript?.delegate = self } @@ -970,28 +1008,6 @@ extension Tab: UserContentControllerDelegate { youtubeOverlayScript?.delegate = self youtubePlayerScript = userScripts.youtubePlayerUserScript setUpYoutubeScriptsIfNeeded() - - findInPageScript = userScripts.findInPageScript - attachFindInPage() - - adClickAttributionLogic.onRulesChanged(latestRules: ContentBlocking.shared.contentBlockingManager.currentRules) - } - -} - -extension Tab: BrowserTabViewControllerClickDelegate { - - func browserTabViewController(_ browserTabViewController: BrowserTabViewController, didClickAtPoint: NSPoint) { - guard let autofillScript = autofillScript else { return } - autofillScript.clickPoint = didClickAtPoint - } - -} - -extension Tab: PrintingUserScriptDelegate { - - func printingUserScriptDidRequestPrintController(_ script: PrintingUserScript) { - self.print() } } @@ -1004,26 +1020,6 @@ extension Tab: PageObserverUserScriptDelegate { } -extension Tab: ContextMenuManagerDelegate { - - func launchSearch(for text: String) { - guard let url = URL.makeSearchUrl(from: text) else { - assertionFailure("Failed to make Search URL") - return - } - - let newTab = Tab(content: .url(url), parentTab: self, shouldLoadInBackground: true) - self.delegate?.tab(self, createdChild: newTab, of: .tab(selected: true)) - } - - func prepareForContextMenuDownload() { - // handling legacy WebKit Downloads for downloads initiated by Context Menu - self.webView.configuration.processPool - .setDownloadDelegateIfNeeded(using: LegacyWebKitDownloadDelegate.init) - } - -} - extension Tab: FaviconUserScriptDelegate { func faviconUserScript(_ faviconUserScript: FaviconUserScript, @@ -1053,7 +1049,6 @@ extension Tab: ContentBlockerRulesUserScriptDelegate { guard let url = webView.url else { return } privacyInfo?.trackerInfo.addDetectedTracker(tracker, onPageWithURL: url) - adClickAttributionLogic.onRequestDetected(request: tracker) historyCoordinating.addDetectedTracker(tracker, onURL: url) } @@ -1076,21 +1071,6 @@ extension HistoryCoordinating { } -extension ContentBlocking { - - func entityName(forDomain domain: String) -> String? { - var entityName: String? - var parts = domain.components(separatedBy: ".") - while parts.count > 1 && entityName == nil { - let host = parts.joined(separator: ".") - entityName = trackerDataManager.trackerData.domains[host] - parts.removeFirst() - } - return entityName - } - -} - extension Tab: ClickToLoadUserScriptDelegate { func clickToLoadUserScriptAllowFB(_ script: UserScript, replyHandler: @escaping (Bool) -> Void) { @@ -1122,104 +1102,6 @@ extension Tab: SurrogatesUserScriptDelegate { } } -extension Tab: AdClickAttributionLogicDelegate { - - func attributionLogic(_ logic: AdClickAttributionLogic, - didRequestRuleApplication rules: ContentBlockerRulesManager.Rules?, - forVendor vendor: String?) { - guard let userContentController = userContentController, - let userScripts = userContentController.contentBlockingAssets?.userScripts as? UserScripts - else { - assertionFailure("UserScripts not loaded") - return - } - - let contentBlockerRulesScript = userScripts.contentBlockerRulesScript - let attributedTempListName = AdClickAttributionRulesProvider.Constants.attributedTempRuleListName - - guard ContentBlocking.shared.privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .contentBlocking) - else { - userContentController.removeLocalContentRuleList(withIdentifier: attributedTempListName) - contentBlockerRulesScript.currentAdClickAttributionVendor = nil - contentBlockerRulesScript.supplementaryTrackerData = [] - return - } - - contentBlockerRulesScript.currentAdClickAttributionVendor = vendor - if let rules = rules { - - let globalListName = DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName - let globalAttributionListName = AdClickAttributionRulesSplitter.blockingAttributionRuleListName(forListNamed: globalListName) - - if vendor != nil { - userContentController.installLocalContentRuleList(rules.rulesList, identifier: attributedTempListName) - try? userContentController.disableGlobalContentRuleList(withIdentifier: globalAttributionListName) - } else { - userContentController.removeLocalContentRuleList(withIdentifier: attributedTempListName) - try? userContentController.enableGlobalContentRuleList(withIdentifier: globalAttributionListName) - } - - contentBlockerRulesScript.supplementaryTrackerData = [rules.trackerData] - } else { - contentBlockerRulesScript.supplementaryTrackerData = [] - } - } - -} - -extension Tab: EmailManagerRequestDelegate { } - -extension Tab: SecureVaultManagerDelegate { - - public func secureVaultManagerIsEnabledStatus(_: SecureVaultManager) -> Bool { - return true - } - - func secureVaultManager(_: SecureVaultManager, promptUserToStoreAutofillData data: AutofillData) { - delegate?.tab(self, requestedSaveAutofillData: data) - } - - func secureVaultManager(_: SecureVaultManager, - promptUserToAutofillCredentialsForDomain domain: String, - withAccounts accounts: [SecureVaultModels.WebsiteAccount], - withTrigger trigger: AutofillUserScript.GetTriggerType, - completionHandler: @escaping (SecureVaultModels.WebsiteAccount?) -> Void) { - // no-op on macOS - } - - func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: String) { - Pixel.fire(.formAutofilled(kind: type.formAutofillKind)) - } - - func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler handler: @escaping (Bool) -> Void) { - DeviceAuthenticator.shared.authenticateUser(reason: .autofill) { authenticationResult in - handler(authenticationResult.authenticated) - } - } - - func secureVaultInitFailed(_ error: SecureVaultError) { - SecureVaultErrorReporter.shared.secureVaultInitFailed(error) - } - - func secureVaultManagerShouldAutomaticallyUpdateCredentialsWithoutUsername(_: SecureVaultManager) -> Bool { - return true - } - - public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { - Pixel.fire(.jsPixel(pixel)) - } -} - -extension AutofillType { - var formAutofillKind: Pixel.Event.FormAutofillKind { - switch self { - case .password: return .password - case .card: return .card - case .identity: return .identity - } - } -} - extension Tab: WKNavigationDelegate { func webView(_ webView: WKWebView, @@ -1301,20 +1183,19 @@ extension Tab: WKNavigationDelegate { let navigationActionPolicy = await linkProtection .requestTrackingLinkRewrite( initiatingURL: webView.url, - navigationAction: navigationAction, + destinationURL: navigationAction.request.url!, onStartExtracting: { if !isRequestingNewTab { isAMPProtectionExtracting = true }}, onFinishExtracting: { [weak self] in self?.isAMPProtectionExtracting = false }, - onLinkRewrite: { [weak self] url, _ in + onLinkRewrite: { [weak self] url in guard let self = self else { return } if isRequestingNewTab || !navigationAction.isTargetingMainFrame { - let tab = Tab(content: .url(url), parentTab: self, shouldLoadInBackground: true) - self.delegate?.tab(self, createdChild: tab, of: .tab(selected: shouldSelectNewTab || !navigationAction.isTargetingMainFrame)) + self.openChild(with: .url(url), of: .tab(selected: shouldSelectNewTab || !navigationAction.isTargetingMainFrame)) } else { webView.load(url) } }) - if let navigationActionPolicy = navigationActionPolicy, navigationActionPolicy == .cancel { - return navigationActionPolicy + if let navigationActionPolicy = navigationActionPolicy, navigationActionPolicy == false { + return .cancel } } @@ -1325,15 +1206,14 @@ extension Tab: WKNavigationDelegate { } if navigationAction.isTargetingMainFrame, navigationAction.navigationType == .backForward { - adClickAttributionLogic.onBackForwardNavigation(mainFrameURL: webView.url) + self.adClickAttribution?.logic.onBackForwardNavigation(mainFrameURL: webView.url) } if navigationAction.isTargetingMainFrame, navigationAction.navigationType != .backForward { if let newRequest = referrerTrimming.trimReferrer(forNavigation: navigationAction, originUrl: webView.url ?? navigationAction.sourceFrame.webView?.url) { if isRequestingNewTab { - let tab = Tab(content: newRequest.url.map { .contentFromURL($0) } ?? .none, parentTab: self, shouldLoadInBackground: true) - self.delegate?.tab(self, createdChild: tab, of: .tab(selected: shouldSelectNewTab)) + self.openChild(with: newRequest.url.map { .contentFromURL($0) } ?? .none, of: .tab(selected: shouldSelectNewTab)) } else { _ = webView.load(newRequest) } @@ -1376,8 +1256,7 @@ extension Tab: WKNavigationDelegate { if isRequestingNewTab { defer { - let tab = Tab(content: navigationAction.request.url.map { .contentFromURL($0) } ?? .none, parentTab: self, shouldLoadInBackground: true) - delegate?.tab(self, createdChild: tab, of: .tab(selected: shouldSelectNewTab)) + self.openChild(with: navigationAction.request.url.map { .contentFromURL($0) } ?? .none, of: .tab(selected: shouldSelectNewTab)) } return .cancel } else if isLinkActivated && NSApp.isOptionPressed && !NSApp.isCommandPressed { @@ -1402,7 +1281,7 @@ extension Tab: WKNavigationDelegate { } if navigationAction.isTargetingMainFrame { - let result = await PrivacyFeatures.httpsUpgrade.upgrade(url: url) + let result = await privacyFeatures.httpsUpgrade.upgrade(url: url) switch result { case let .success(upgradedURL): if lastUpgradedURL != upgradedURL { @@ -1491,7 +1370,7 @@ extension Tab: WKNavigationDelegate { private func toggleFBProtection(for url: URL) { // Enable/disable FBProtection only after UserScripts are installed (awaitContentBlockingAssetsInstalled) - let privacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig + let privacyConfiguration = contentBlocking.privacyConfigurationManager.privacyConfig let featureEnabled = privacyConfiguration.isFeature(.clickToPlay, enabledForDomain: url.host) setFBProtection(enabled: featureEnabled) @@ -1540,10 +1419,10 @@ extension Tab: WKNavigationDelegate { } if navigationResponse.isForMainFrame && isSuccessfulResponse { - adClickAttributionDetection.on2XXResponse(url: webView.url) + self.adClickAttribution?.detection.on2XXResponse(url: webView.url) } - await adClickAttributionLogic.onProvisionalNavigation() + await self.adClickAttribution?.logic.onProvisionalNavigation() return .allow } @@ -1560,7 +1439,7 @@ extension Tab: WKNavigationDelegate { linkProtection.cancelOngoingExtraction() linkProtection.setMainFrameUrl(webView.url) referrerTrimming.onBeginNavigation(to: webView.url) - adClickAttributionDetection.onStartNavigation(url: webView.url) + self.adClickAttribution?.detection.onStartNavigation(url: webView.url) } @MainActor @@ -1571,8 +1450,8 @@ extension Tab: WKNavigationDelegate { if isAMPProtectionExtracting { isAMPProtectionExtracting = false } linkProtection.setMainFrameUrl(nil) referrerTrimming.onFinishNavigation() - adClickAttributionDetection.onDidFinishNavigation(url: webView.url) - adClickAttributionLogic.onDidFinishNavigation(host: webView.url?.host) + self.adClickAttribution?.detection.onDidFinishNavigation(url: webView.url) + self.adClickAttribution?.logic.onDidFinishNavigation(host: webView.url?.host) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { @@ -1584,7 +1463,7 @@ extension Tab: WKNavigationDelegate { invalidateSessionStateData() linkProtection.setMainFrameUrl(nil) referrerTrimming.onFailedNavigation() - adClickAttributionDetection.onDidFailNavigation() + self.adClickAttribution?.detection.onDidFailNavigation() webViewDidFailNavigationPublisher.send() } @@ -1601,7 +1480,7 @@ extension Tab: WKNavigationDelegate { isBeingRedirected = false linkProtection.setMainFrameUrl(nil) referrerTrimming.onFailedNavigation() - adClickAttributionDetection.onDidFailNavigation() + self.adClickAttribution?.detection.onDidFailNavigation() webViewDidFailNavigationPublisher.send() } @@ -1670,7 +1549,7 @@ extension Tab: WKNavigationDelegate { @objc(_webView:contextMenuDidCreateDownload:) func webView(_ webView: WKWebView, contextMenuDidCreate download: WebKitDownload) { let location: FileDownloadManager.DownloadLocationPreference - = contextMenuManager.shouldAskForDownloadLocation() == false ? .auto : .prompt + = self.contextMenuManager?.shouldAskForDownloadLocation() == false ? .auto : .prompt FileDownloadManager.shared.add(download, delegate: self, location: location, postflight: .none) } @@ -1696,25 +1575,6 @@ extension Tab: FileDownloadManagerDelegate { } -extension Tab { - - private func find(text: String) { - findInPageScript?.find(text: text, inWebView: webView) - } - - func findDone() { - findInPageScript?.done(withWebView: webView) - } - - func findNext() { - findInPageScript?.next(withWebView: webView) - } - - func findPrevious() { - findInPageScript?.previous(withWebView: webView) - } -} - fileprivate extension WKNavigationResponse { var shouldDownload: Bool { let contentDisposition = (response as? HTTPURLResponse)?.allHeaderFields["Content-Disposition"] as? String @@ -1722,14 +1582,6 @@ fileprivate extension WKNavigationResponse { } } -extension Tab: HoverUserScriptDelegate { - - func hoverUserScript(_ script: HoverUserScript, didChange url: URL?) { - delegate?.tab(self, didChangeHoverLink: url) - } - -} - @available(macOS 11, *) extension Tab: AutoconsentUserScriptDelegate { func autoconsentUserScript(consentStatus: CookieConsentInfo) { @@ -1747,8 +1599,7 @@ extension Tab: YoutubeOverlayUserScriptDelegate { let isRequestingNewTab = NSApp.isCommandPressed if isRequestingNewTab { let shouldSelectNewTab = NSApp.isShiftPressed - let tab = Tab(content: content, parentTab: self, shouldLoadInBackground: true) - self.delegate?.tab(self, createdChild: tab, of: .tab(selected: shouldSelectNewTab)) + self.openChild(with: content, of: .tab(selected: shouldSelectNewTab)) } else { setContent(content) } diff --git a/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift b/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift new file mode 100644 index 0000000000..e431422df2 --- /dev/null +++ b/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift @@ -0,0 +1,92 @@ +// +// TabExtensionsBuilder.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 Combine +import Foundation + +protocol TabExtensionsBuilder { + var components: [any TabExtension] { get set } + mutating func make(with dependencies: TabExtensionDependencies) + func build(with dependencies: TabExtensionDependencies) -> TabExtensions +} + +extension TabExtensionsBuilder { + + @discardableResult + mutating func add(_ makeTabExtension: () -> T) -> T { + let tabExtension = makeTabExtension() + components.append(tabExtension) + return tabExtension + } + + func build(with dependencies: TabExtensionDependencies) -> TabExtensions { + var builder = self + builder.make(with: dependencies) + return TabExtensions(components: builder.components) + } + +} + +struct AppTabExtensions: TabExtensionsBuilder { + var components = [any TabExtension]() +} +struct TestTabExtensions: TabExtensionsBuilder { + var components = [any TabExtension]() +} + +struct TabExtensions { + typealias ExtensionType = TabExtension + + private(set) var extensions: [AnyKeyPath: any TabExtension] + + static func builder() -> TabExtensionsBuilder { +#if DEBUG + return AppDelegate.isRunningTests ? TestTabExtensions() : AppTabExtensions() +#else + return AppTabExtensions() +#endif + } + + init(components: [any TabExtension]) { + var extensions = [AnyKeyPath: any TabExtension]() + func add(_ tabExtension: T) { + assert(extensions[\T.self] == nil) + extensions[\T.self] = tabExtension + } + components.forEach { add($0) } + self.extensions = extensions + } + + func resolve(_: T.Type) -> T.PublicProtocol? { + (extensions[\T.self] as? T)?.getPublicProtocol() + } + + func resolve(_: T.Type) -> T.PublicProtocol? where T.PublicProtocol == T { + fatalError("ok, please don‘t cheat") + } + +} + +extension TabExtensions: Sequence { + typealias Iterator = Dictionary.Values.Iterator + + func makeIterator() -> Iterator { + self.extensions.values.makeIterator() + } + +} diff --git a/DuckDuckGo/Browser Tab/Model/UserContentUpdating.swift b/DuckDuckGo/Browser Tab/Model/UserContentUpdating.swift index dce85ce462..2611d91507 100644 --- a/DuckDuckGo/Browser Tab/Model/UserContentUpdating.swift +++ b/DuckDuckGo/Browser Tab/Model/UserContentUpdating.swift @@ -18,6 +18,7 @@ import Foundation import Combine +import Common import BrowserServicesKit import UserScript @@ -35,15 +36,19 @@ final class UserContentUpdating { private(set) var userContentBlockingAssets: AnyPublisher! init(contentBlockerRulesManager: ContentBlockerRulesManagerProtocol, - privacyConfigurationManager: PrivacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager, - configStorage: ConfigurationStoring = DefaultConfigurationStorage.shared, - privacySecurityPreferences: PrivacySecurityPreferences = PrivacySecurityPreferences.shared) { + privacyConfigurationManager: PrivacyConfigurationManaging, + trackerDataManager: TrackerDataManager, + configStorage: ConfigurationStoring, + privacySecurityPreferences: PrivacySecurityPreferences, + tld: TLD) { let makeValue: (ContentBlockerRulesManager.UpdateEvent) -> NewContent = { rulesUpdate in - let sourceProvider = DefaultScriptSourceProvider(configStorage: configStorage, - privacyConfigurationManager: privacyConfigurationManager, - privacySettings: privacySecurityPreferences, - contentBlockingManager: contentBlockerRulesManager) + let sourceProvider = ScriptSourceProvider(configStorage: configStorage, + privacyConfigurationManager: privacyConfigurationManager, + privacySettings: privacySecurityPreferences, + contentBlockingManager: contentBlockerRulesManager, + trackerDataManager: trackerDataManager, + tld: tld) return NewContent(rulesUpdate: rulesUpdate, sourceProvider: sourceProvider) } diff --git a/DuckDuckGo/Browser Tab/Model/ContextMenuUserScript.swift b/DuckDuckGo/Browser Tab/UserScripts/ContextMenuUserScript.swift similarity index 99% rename from DuckDuckGo/Browser Tab/Model/ContextMenuUserScript.swift rename to DuckDuckGo/Browser Tab/UserScripts/ContextMenuUserScript.swift index d539fd32d6..ca78d34588 100644 --- a/DuckDuckGo/Browser Tab/Model/ContextMenuUserScript.swift +++ b/DuckDuckGo/Browser Tab/UserScripts/ContextMenuUserScript.swift @@ -42,7 +42,6 @@ final class ContextMenuUserScript: NSObject, StaticUserScript { document.addEventListener("contextmenu", function(e) { webkit.messageHandlers.contextMenu.postMessage(window.getSelection().toString()); }, true); - }) (); """ diff --git a/DuckDuckGo/Browser Tab/Model/DebugUserScript.swift b/DuckDuckGo/Browser Tab/UserScripts/DebugUserScript.swift similarity index 100% rename from DuckDuckGo/Browser Tab/Model/DebugUserScript.swift rename to DuckDuckGo/Browser Tab/UserScripts/DebugUserScript.swift diff --git a/DuckDuckGo/Browser Tab/Model/HoverUserScript.swift b/DuckDuckGo/Browser Tab/UserScripts/HoverUserScript.swift similarity index 100% rename from DuckDuckGo/Browser Tab/Model/HoverUserScript.swift rename to DuckDuckGo/Browser Tab/UserScripts/HoverUserScript.swift diff --git a/DuckDuckGo/Browser Tab/Model/PrintingUserScript.swift b/DuckDuckGo/Browser Tab/UserScripts/PrintingUserScript.swift similarity index 83% rename from DuckDuckGo/Browser Tab/Model/PrintingUserScript.swift rename to DuckDuckGo/Browser Tab/UserScripts/PrintingUserScript.swift index 219027598a..b4373b4fcb 100644 --- a/DuckDuckGo/Browser Tab/Model/PrintingUserScript.swift +++ b/DuckDuckGo/Browser Tab/UserScripts/PrintingUserScript.swift @@ -21,7 +21,7 @@ import UserScript public protocol PrintingUserScriptDelegate: AnyObject { - func printingUserScriptDidRequestPrintController(_ script: PrintingUserScript) + func runPrintOperation(for frameHandle: Any?, in webView: WKWebView, completionHandler: ((Bool) -> Void)?) } @@ -45,7 +45,8 @@ public class PrintingUserScript: NSObject, UserScript { public var messageNames: [String] = ["printHandler"] public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - delegate?.printingUserScriptDidRequestPrintController(self) + guard let webView = message.webView else { return } + delegate?.runPrintOperation(for: message.frameInfo.handle, in: webView, completionHandler: nil) } } diff --git a/DuckDuckGo/Browser Tab/Model/UserScripts.swift b/DuckDuckGo/Browser Tab/UserScripts/UserScripts.swift similarity index 100% rename from DuckDuckGo/Browser Tab/Model/UserScripts.swift rename to DuckDuckGo/Browser Tab/UserScripts/UserScripts.swift diff --git a/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift index a882cf15cb..1fde69cf0b 100644 --- a/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift @@ -23,10 +23,6 @@ import Combine import SwiftUI import BrowserServicesKit -protocol BrowserTabViewControllerClickDelegate: AnyObject { - func browserTabViewController(_ browserTabViewController: BrowserTabViewController, didClickAtPoint: CGPoint) -} - final class BrowserTabViewController: NSViewController { @IBOutlet weak var errorView: NSView! @@ -45,6 +41,7 @@ final class BrowserTabViewController: NSViewController { private var userDialogsCancellable: AnyCancellable? private var activeUserDialogCancellable: ModalSheetCancellable? private var errorViewStateCancellable: AnyCancellable? + private var hoverLinkCancellable: AnyCancellable? private var pinnedTabsDelegatesCancellable: AnyCancellable? private var keyWindowSelectedTabCancellable: AnyCancellable? private var cancellables = Set() @@ -120,6 +117,7 @@ final class BrowserTabViewController: NSViewController { self.showTabContent(of: selectedTabViewModel) self.subscribeToErrorViewState() self.subscribeToTabContent(of: selectedTabViewModel) + self.subscribeToHoveredLink(of: selectedTabViewModel) self.showCookieConsentPopoverIfNecessary(selectedTabViewModel) self.subscribeToUserDialogs(of: selectedTabViewModel) } @@ -138,21 +136,23 @@ final class BrowserTabViewController: NSViewController { private func subscribeToTabs() { tabCollectionViewModel.tabCollection.$tabs - .sink { [weak self] tabs in - for tab in tabs where tab.delegate !== self { - tab.delegate = self - } - } + .sink(receiveValue: setDelegate()) .store(in: &cancellables) } private func subscribeToPinnedTabs() { pinnedTabsDelegatesCancellable = tabCollectionViewModel.pinnedTabsCollection?.$tabs - .sink { [weak self] tabs in - for tab in tabs where tab.delegate !== self { - tab.delegate = self - } + .sink(receiveValue: setDelegate()) + } + + private func setDelegate() -> ([Tab]) -> Void { + { [weak self] (tabs: [Tab]) in + guard let self else { return } + for tab in tabs { + tab.setDelegate(self) + tab.autofill?.setDelegate(self) } + } } private func removeWebViewFromHierarchy(webView: WebView? = nil, @@ -268,6 +268,12 @@ final class BrowserTabViewController: NSViewController { } } + func subscribeToHoveredLink(of tabViewModel: TabViewModel?) { + hoverLinkCancellable = tabViewModel?.tab.hoveredLinkPublisher.sink { [weak self] in + self?.scheduleHoverLabelUpdatesForUrl($0) + } + } + func makeWebViewFirstResponder() { if let webView = self.webView { webView.makeMeFirstResponder() @@ -300,9 +306,8 @@ final class BrowserTabViewController: NSViewController { // shouldn't open New Tabs in PopUp window guard view.window?.isPopUpWindow == false else { // Prefer Tab's Parent - if let parentTab = tabCollectionViewModel.selectedTabViewModel?.tab.parentTab, parentTab.delegate !== self { - let tab = Tab(content: content, parentTab: parentTab, shouldLoadInBackground: true) - parentTab.delegate?.tab(parentTab, createdChild: tab, of: .tab(selected: true)) + if let parentTab = tabCollectionViewModel.selectedTabViewModel?.tab.parentTab { + parentTab.openChild(with: content, of: .tab(selected: true)) parentTab.webView.window?.makeKeyAndOrderFront(nil) // Act as default URL Handler if no Parent } else { @@ -339,8 +344,8 @@ final class BrowserTabViewController: NSViewController { private func removeAllTabContent(includingWebView: Bool = true) { self.homePageView.removeFromSuperview() transientTabContentViewController?.removeCompletely() - preferencesViewController.removeCompletely() - bookmarksViewController.removeCompletely() + preferencesViewController?.removeCompletely() + bookmarksViewController?.removeCompletely() if includingWebView { self.removeWebViewFromHierarchy() } @@ -371,10 +376,11 @@ final class BrowserTabViewController: NSViewController { switch tabViewModel?.tab.content { case .bookmarks: removeAllTabContent() - showTabContentController(bookmarksViewController) + showTabContentController(bookmarksViewControllerCreatingIfNeeded()) case let .preferences(pane): removeAllTabContent() + let preferencesViewController = preferencesViewControllerCreatingIfNeeded() if let pane = pane, preferencesViewController.model.selectedPane != pane { preferencesViewController.model.selectPane(pane) } @@ -418,62 +424,55 @@ final class BrowserTabViewController: NSViewController { // MARK: - Preferences - private(set) lazy var preferencesViewController: PreferencesViewController = { - let viewController = PreferencesViewController() - viewController.delegate = self - - return viewController - }() + var preferencesViewController: PreferencesViewController? + private func preferencesViewControllerCreatingIfNeeded() -> PreferencesViewController { + return preferencesViewController ?? { + let preferencesViewController = PreferencesViewController() + preferencesViewController.delegate = self + self.preferencesViewController = preferencesViewController + return preferencesViewController + }() + } // MARK: - Bookmarks - private(set) lazy var bookmarksViewController: BookmarkManagementSplitViewController = { - let viewController = BookmarkManagementSplitViewController.create() - viewController.delegate = self - - return viewController - }() - - private var _contentOverlayPopover: ContentOverlayPopover? - public var contentOverlayPopover: ContentOverlayPopover { - guard let overlay = _contentOverlayPopover else { - let overlayPopover = ContentOverlayPopover(currentTabView: view) + var bookmarksViewController: BookmarkManagementSplitViewController? + private func bookmarksViewControllerCreatingIfNeeded() -> BookmarkManagementSplitViewController { + return bookmarksViewController ?? { + let bookmarksViewController = BookmarkManagementSplitViewController.create() + bookmarksViewController.delegate = self + self.bookmarksViewController = bookmarksViewController + return bookmarksViewController + }() + } + + private var contentOverlayPopover: ContentOverlayPopover? + private func contentOverlayPopoverCreatingIfNeeded() -> ContentOverlayPopover { + return contentOverlayPopover ?? { + let overlayPopover = ContentOverlayPopover(currentTabView: self.view) + self.contentOverlayPopover = overlayPopover WindowControllersManager.shared.stateChanged - .sink { [weak self] _ in - self?._contentOverlayPopover?.websiteAutofillUserScriptCloseOverlay(nil) - }.store(in: &cancellables) - _contentOverlayPopover = overlayPopover + .sink { [weak overlayPopover] _ in + overlayPopover?.websiteAutofillUserScriptCloseOverlay(nil) + }.store(in: &self.cancellables) return overlayPopover - } - return overlay - } - - @objc(_webView:printFrame:) - func webView(_ webView: WKWebView, printFrame handle: Any) { - webView.tab?.print(frame: handle) - } - - @available(macOS 12, *) - @objc(_webView:printFrame:pdfFirstPageSize:completionHandler:) - func webView(_ webView: WKWebView, printFrame handle: Any, pdfFirstPageSize size: CGSize, completionHandler: () -> Void) { - self.webView(webView, printFrame: handle) - completionHandler() + }() } } extension BrowserTabViewController: ContentOverlayUserScriptDelegate { public func websiteAutofillUserScriptCloseOverlay(_ websiteAutofillUserScript: WebsiteAutofillUserScript?) { - contentOverlayPopover.websiteAutofillUserScriptCloseOverlay(websiteAutofillUserScript) + contentOverlayPopoverCreatingIfNeeded().websiteAutofillUserScriptCloseOverlay(websiteAutofillUserScript) } public func websiteAutofillUserScript(_ websiteAutofillUserScript: WebsiteAutofillUserScript, willDisplayOverlayAtClick: NSPoint?, serializedInputContext: String, inputPosition: CGRect) { - contentOverlayPopover.websiteAutofillUserScript(websiteAutofillUserScript, - willDisplayOverlayAtClick: willDisplayOverlayAtClick, - serializedInputContext: serializedInputContext, - inputPosition: inputPosition) + contentOverlayPopoverCreatingIfNeeded().websiteAutofillUserScript(websiteAutofillUserScript, + willDisplayOverlayAtClick: willDisplayOverlayAtClick, + serializedInputContext: serializedInputContext, + inputPosition: inputPosition) } } @@ -565,10 +564,6 @@ extension BrowserTabViewController: TabDelegate { } } - func tab(_ tab: Tab, didChangeHoverLink url: URL?) { - scheduleHoverLabelUpdatesForUrl(url) - } - func windowDidBecomeKey() { keyWindowSelectedTabCancellable = nil subscribeToPinnedTabs() @@ -911,7 +906,7 @@ extension BrowserTabViewController { func mouseDown(with event: NSEvent) -> NSEvent? { guard event.window === self.view.window else { return event } - tabViewModel?.tab.browserTabViewController(self, didClickAtPoint: event.locationInWindow) + tabViewModel?.tab.autofill?.didClick(at: event.locationInWindow) return event } } diff --git a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift index f60f31c9b3..92500d9064 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift @@ -74,7 +74,7 @@ final class TabViewModel { @Published private(set) var title: String = UserText.tabHomeTitle @Published private(set) var favicon: NSImage? - @Published private(set) var findInPage: FindInPageModel = FindInPageModel() + var findInPage: FindInPageModel? { tab.findInPage?.model } @Published private(set) var usedPermissions = Permissions() @Published private(set) var permissionAuthorizationQuery: PermissionAuthorizationQuery? @@ -306,23 +306,20 @@ final class TabViewModel { extension TabViewModel { - func startFindInPage() { - tab.findInPage = findInPage - findInPage.show() + func showFindInPage() { + tab.findInPage?.show(with: tab.webView) } func closeFindInPage() { - guard findInPage.visible else { return } - tab.findDone() - findInPage.hide() + tab.findInPage?.close() } func findInPageNext() { - tab.findNext() + tab.findInPage?.findNext() } func findInPagePrevious() { - tab.findPrevious() + tab.findInPage?.findPrevious() } } diff --git a/DuckDuckGo/Common/Extensions/OptionalExtension.swift b/DuckDuckGo/Common/Extensions/OptionalExtension.swift index acecb9612f..f7e60fc9c9 100644 --- a/DuckDuckGo/Common/Extensions/OptionalExtension.swift +++ b/DuckDuckGo/Common/Extensions/OptionalExtension.swift @@ -19,8 +19,17 @@ import Foundation protocol OptionalProtocol { + associatedtype Wrapped + var isNil: Bool { get } + + /// instantiate a Concrete-Typed `Optional.none as T` from an `AnyOptionalType` + /// can be used to return nil value for a maybe-optional Generic Type + /// usage: `(T.self as? AnyOptionalType)?.none as? T` + static var none: Self { get } } +typealias AnyOptional = any OptionalProtocol +typealias AnyOptionalType = any OptionalProtocol.Type extension Optional: OptionalProtocol { diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index 7114d55f33..75932f83b8 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -41,6 +41,12 @@ extension String { .init(self[utf16.index(startIndex, offsetBy: range.lowerBound) ..< utf16.index(startIndex, offsetBy: range.upperBound)]) } + func escapedJavaScriptString() -> String { + self.replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "'", with: "\\'") + } + // MARK: - URL var url: URL? { diff --git a/DuckDuckGo/Common/Extensions/WKFrameInfoExtension.swift b/DuckDuckGo/Common/Extensions/WKFrameInfoExtension.swift new file mode 100644 index 0000000000..508b8574ce --- /dev/null +++ b/DuckDuckGo/Common/Extensions/WKFrameInfoExtension.swift @@ -0,0 +1,32 @@ +// +// WKFrameInfoExtension.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 WebKit + +extension WKFrameInfo { + + private static let _handle = "_handle" + var handle: Any? { + guard self.responds(to: NSSelectorFromString(Self._handle)) else { + assertionFailure("WKFrameInfo does not respond to _handle") + return nil + } + return self.value(forKey: Self._handle) + } + +} diff --git a/DuckDuckGo/Browser Tab/Model/WKMenuItemIdentifier.swift b/DuckDuckGo/Common/Extensions/WKMenuItemIdentifier.swift similarity index 99% rename from DuckDuckGo/Browser Tab/Model/WKMenuItemIdentifier.swift rename to DuckDuckGo/Common/Extensions/WKMenuItemIdentifier.swift index 082c9265b3..eb4f388879 100644 --- a/DuckDuckGo/Browser Tab/Model/WKMenuItemIdentifier.swift +++ b/DuckDuckGo/Common/Extensions/WKMenuItemIdentifier.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import AppKit enum WKMenuItemIdentifier: String, CaseIterable { case copy = "WKMenuItemIdentifierCopy" diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index 922f6a5a6c..48eff6db4f 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -22,10 +22,7 @@ import BrowserServicesKit extension WKWebViewConfiguration { - func applyStandardConfiguration() { -#if DEBUG - guard !AppDelegate.isRunningTests else { return } -#endif + func applyStandardConfiguration(contentBlocking: some ContentBlockingProtocol) { allowsAirPlayForMediaPlayback = true preferences.setValue(true, forKey: "fullScreenEnabled") @@ -39,22 +36,17 @@ extension WKWebViewConfiguration { preferences.javaScriptCanOpenWindowsAutomatically = false } preferences.isFraudulentWebsiteWarningEnabled = false - + if urlSchemeHandler(forURLScheme: PrivatePlayer.privatePlayerScheme) == nil { setURLSchemeHandler(PrivatePlayerSchemeHandler(), forURLScheme: PrivatePlayer.privatePlayerScheme) } - self.userContentController = UserContentController() + let userContentController = UserContentController(assetsPublisher: contentBlocking.contentBlockingAssetsPublisher, + privacyConfigurationManager: contentBlocking.privacyConfigurationManager) + + self.userContentController = userContentController self.processPool.geolocationProvider = GeolocationProvider(processPool: self.processPool) + self.processPool.setDownloadDelegateIfNeeded(using: LegacyWebKitDownloadDelegate.init) } } - -extension UserContentController { - - convenience init(privacyConfigurationManager: PrivacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager) { - self.init(assetsPublisher: ContentBlocking.shared.userContentUpdating.userContentBlockingAssets, - privacyConfigurationManager: privacyConfigurationManager) - } - -} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 988355ca50..21b54052ea 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -192,6 +192,11 @@ extension WKWebView { } } + func loadInNewWindow(_ url: URL) { + let urlEnc = "'\(url.absoluteString.escapedJavaScriptString())'" + self.evaluateJavaScript("window.open(\(urlEnc), '_blank', 'noopener, noreferrer')") + } + func getMimeType(callback: @escaping (String?) -> Void) { self.evaluateJavaScript("document.contentType") { (result, _) in callback(result as? String) diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 81aceb96b6..d733cf11b7 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -124,7 +124,7 @@ public struct UserDefaultsWrapper { return defaultValue } set { - if (newValue as? OptionalProtocol)?.isNil == true { + if (newValue as? AnyOptional)?.isNil == true { UserDefaults.standard.removeObject(forKey: key.rawValue) } else { UserDefaults.standard.set(newValue, forKey: key.rawValue) diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index 6978977980..63f84ff1ff 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -154,9 +154,9 @@ final class ConfigurationManager { let configEtag = DefaultConfigurationStorage.shared.loadEtag(for: .privacyConfiguration) let configData = DefaultConfigurationStorage.shared.loadData(for: .privacyConfiguration) - ContentBlocking.shared.privacyConfigurationManager.reload(etag: configEtag, data: configData) + _=ContentBlocking.shared.privacyConfigurationManager.reload(etag: configEtag, data: configData) - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + _=ContentBlocking.shared.contentBlockingManager.scheduleCompilation() } private func updateBloomFilter() throws { diff --git a/DuckDuckGo/Content Blocker/ContentBlocking.swift b/DuckDuckGo/Content Blocker/ContentBlocking.swift index 84aa90b1d8..cc6ed32fb4 100644 --- a/DuckDuckGo/Content Blocker/ContentBlocking.swift +++ b/DuckDuckGo/Content Blocker/ContentBlocking.swift @@ -23,24 +23,42 @@ import os.log import BrowserServicesKit import Common -final class ContentBlocking { - static let shared = ContentBlocking() +protocol ContentBlockingProtocol { - let privacyConfigurationManager: PrivacyConfigurationManager + var privacyConfigurationManager: PrivacyConfigurationManaging { get } + var contentBlockingManager: ContentBlockerRulesManagerProtocol { get } + var trackerDataManager: TrackerDataManager { get } + var tld: TLD { get } + + var contentBlockingAssetsPublisher: AnyPublisher { get } + +} + +typealias AnyContentBlocking = any ContentBlockingProtocol & AdClickAttributionDependencies + +// refactor: ContentBlocking.shared to be removed, ContentBlockingProtocol to be renamed to ContentBlocking +// ContentBlocking to be passed to init methods as `some ContentBlocking` +typealias ContentBlocking = AppContentBlocking +extension ContentBlocking { + static var shared: AnyContentBlocking { PrivacyFeatures.contentBlocking } +} + +final class AppContentBlocking { + let privacyConfigurationManager: PrivacyConfigurationManaging let trackerDataManager: TrackerDataManager - let contentBlockingManager: ContentBlockerRulesManager + let contentBlockingManager: ContentBlockerRulesManagerProtocol let userContentUpdating: UserContentUpdating let tld = TLD() - + let adClickAttribution: AdClickAttributing - let adClickAttributionRulesProvider: AdClickAttributionRulesProvider + let adClickAttributionRulesProvider: AdClickAttributionRulesProviding private let contentBlockerRulesSource: ContentBlockerRulesLists private let exceptionsSource: DefaultContentBlockerRulesExceptionsSource // keeping whole ContentBlocking state initialization in one place to avoid races between updates publishing and rules storing - private init() { + init() { let configStorage = DefaultConfigurationStorage.shared privacyConfigurationManager = PrivacyConfigurationManager(fetchedETag: configStorage.loadEtag(for: .privacyConfiguration), fetchedData: configStorage.loadData(for: .privacyConfiguration), @@ -65,7 +83,10 @@ final class ContentBlocking { logger: OSLog.contentBlocking) userContentUpdating = UserContentUpdating(contentBlockerRulesManager: contentBlockingManager, privacyConfigurationManager: privacyConfigurationManager, - configStorage: configStorage) + trackerDataManager: trackerDataManager, + configStorage: configStorage, + privacySecurityPreferences: PrivacySecurityPreferences.shared, + tld: tld) adClickAttributionRulesProvider = AdClickAttributionRulesProvider(config: adClickAttribution, compiledRulesSource: contentBlockingManager, @@ -128,25 +149,8 @@ final class ContentBlocking { } // MARK: - Ad Click Attribution - - public func makeAdClickAttributionDetection() -> AdClickAttributionDetection { - AdClickAttributionDetection(feature: adClickAttribution, - tld: tld, - eventReporting: attributionEvents, - errorReporting: attributionDebugEvents, - log: OSLog.attribution) - } - - public func makeAdClickAttributionLogic() -> AdClickAttributionLogic { - AdClickAttributionLogic(featureConfig: adClickAttribution, - rulesProvider: adClickAttributionRulesProvider, - tld: tld, - eventReporting: attributionEvents, - errorReporting: attributionDebugEvents, - log: OSLog.attribution) - } - - private let attributionEvents = EventMapping { event, _, parameters, _ in + + let attributionEvents: EventMapping? = .init { event, _, parameters, _ in let domainEvent: Pixel.Event switch event { case .adAttributionDetected: @@ -158,7 +162,7 @@ final class ContentBlocking { Pixel.fire(domainEvent, withAdditionalParameters: parameters ?? [:]) } - private let attributionDebugEvents = EventMapping { event, _, _, _ in + let attributionDebugEvents: EventMapping? = .init { event, _, _, _ in let domainEvent: Pixel.Event.Debug switch event { case .adAttributionCompilationFailedForAttributedRulesList: @@ -188,10 +192,12 @@ final class ContentBlocking { } } -protocol ContentBlockerRulesManagerProtocol: AnyObject { +protocol ContentBlockerRulesManagerProtocol: CompiledRuleListsSource { var updatesPublisher: AnyPublisher { get } var currentRules: [ContentBlockerRulesManager.Rules] { get } + func scheduleCompilation() -> ContentBlockerRulesManager.CompletionToken } + extension ContentBlockerRulesManager: ContentBlockerRulesManagerProtocol {} final class ContentBlockingRulesCache: ContentBlockerRulesCaching { @@ -204,3 +210,11 @@ final class ContentBlockingRulesCache: ContentBlockerRulesCaching { } } + +extension AppContentBlocking: ContentBlockingProtocol { + + var contentBlockingAssetsPublisher: AnyPublisher { + self.userContentUpdating.userContentBlockingAssets + } + +} diff --git a/DuckDuckGo/Content Blocker/ScriptSourceProviding.swift b/DuckDuckGo/Content Blocker/ScriptSourceProviding.swift index 49bbc74ed4..eda8e99dae 100644 --- a/DuckDuckGo/Content Blocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/Content Blocker/ScriptSourceProviding.swift @@ -18,13 +18,14 @@ import Foundation import Combine +import Common import BrowserServicesKit protocol ScriptSourceProviding { var contentBlockerRulesConfig: ContentBlockerUserScriptConfig? { get } var surrogatesConfig: SurrogatesUserScriptConfig? { get } - var privacyConfigurationManager: PrivacyConfigurationManager { get } + var privacyConfigurationManager: PrivacyConfigurationManaging { get } var autofillSourceProvider: AutofillUserScriptSourceProvider? { get } var sessionKey: String? { get } var clickToLoadSource: String { get } @@ -32,7 +33,13 @@ protocol ScriptSourceProviding { } -struct DefaultScriptSourceProvider: ScriptSourceProviding { +// refactor: ScriptSourceProvider to be passed to init methods as `some ScriptSourceProviding`, DefaultScriptSourceProvider to be killed +// swiftlint:disable:next identifier_name +func DefaultScriptSourceProvider() -> ScriptSourceProviding { + ScriptSourceProvider(configStorage: DefaultConfigurationStorage.shared, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, privacySettings: PrivacySecurityPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) +} + +struct ScriptSourceProvider: ScriptSourceProviding { private(set) var contentBlockerRulesConfig: ContentBlockerUserScriptConfig? private(set) var surrogatesConfig: SurrogatesUserScriptConfig? @@ -41,19 +48,25 @@ struct DefaultScriptSourceProvider: ScriptSourceProviding { private(set) var clickToLoadSource: String = "" let configStorage: ConfigurationStoring - let privacyConfigurationManager: PrivacyConfigurationManager + let privacyConfigurationManager: PrivacyConfigurationManaging let contentBlockingManager: ContentBlockerRulesManagerProtocol + let trackerDataManager: TrackerDataManager let privacySettings: PrivacySecurityPreferences + let tld: TLD - init(configStorage: ConfigurationStoring = DefaultConfigurationStorage.shared, - privacyConfigurationManager: PrivacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager, - privacySettings: PrivacySecurityPreferences = PrivacySecurityPreferences.shared, - contentBlockingManager: ContentBlockerRulesManagerProtocol = ContentBlocking.shared.contentBlockingManager) { + init(configStorage: ConfigurationStoring, + privacyConfigurationManager: PrivacyConfigurationManaging, + privacySettings: PrivacySecurityPreferences, + contentBlockingManager: ContentBlockerRulesManagerProtocol, + trackerDataManager: TrackerDataManager, + tld: TLD) { self.configStorage = configStorage self.privacyConfigurationManager = privacyConfigurationManager self.privacySettings = privacySettings self.contentBlockingManager = contentBlockingManager + self.trackerDataManager = trackerDataManager + self.tld = tld self.contentBlockerRulesConfig = buildContentBlockerRulesConfig() self.surrogatesConfig = buildSurrogatesConfig() @@ -65,7 +78,7 @@ struct DefaultScriptSourceProvider: ScriptSourceProviding { private func generateSessionKey() -> String { return UUID().uuidString } - + public func buildAutofillSource() -> AutofillUserScriptSourceProvider { return DefaultAutofillSourceProvider(privacyConfigurationManager: self.privacyConfigurationManager, @@ -86,18 +99,18 @@ struct DefaultScriptSourceProvider: ScriptSourceProviding { return DefaultContentBlockerUserScriptConfig(privacyConfiguration: privacyConfigurationManager.privacyConfig, trackerData: trackerData, ctlTrackerData: ctlTrackerData, - tld: ContentBlocking.shared.tld, - trackerDataManager: ContentBlocking.shared.trackerDataManager) + tld: tld, + trackerDataManager: trackerDataManager) } private func buildSurrogatesConfig() -> SurrogatesUserScriptConfig { let isDebugBuild: Bool - #if DEBUG +#if DEBUG isDebugBuild = true - #else +#else isDebugBuild = false - #endif +#endif let surrogates = configStorage.loadData(for: .surrogates)?.utf8String() ?? "" let tdsName = DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName @@ -106,8 +119,8 @@ struct DefaultScriptSourceProvider: ScriptSourceProviding { surrogates: surrogates, trackerData: rules?.trackerData, encodedSurrogateTrackerData: rules?.encodedTrackerData, - trackerDataManager: ContentBlocking.shared.trackerDataManager, - tld: ContentBlocking.shared.tld, + trackerDataManager: trackerDataManager, + tld: tld, isDebugBuild: isDebugBuild) } diff --git a/DuckDuckGo/Find In Page/FindInPageModel.swift b/DuckDuckGo/Find In Page/FindInPageModel.swift index aae2898e83..fa17fb1066 100644 --- a/DuckDuckGo/Find In Page/FindInPageModel.swift +++ b/DuckDuckGo/Find In Page/FindInPageModel.swift @@ -17,29 +17,54 @@ // import Foundation +import WebKit final class FindInPageModel { @Published private(set) var text: String = "" @Published private(set) var currentSelection: Int = 1 @Published private(set) var matchesFound: Int = 0 - @Published private(set) var visible: Bool = false + @Published private(set) var isVisible: Bool = false - func update(text: String) { - self.text = text - } + weak var webView: WKWebView? func update(currentSelection: Int?, matchesFound: Int?) { self.currentSelection = currentSelection ?? self.currentSelection self.matchesFound = matchesFound ?? self.matchesFound } - func show() { - visible = true + func show(with webView: WKWebView) { + self.webView = webView + isVisible = true + } + + func close() { + isVisible = false + } + + func find(_ text: String) { + self.text = text + evaluate("window.__firefox__.find('\(text.escapedJavaScriptString())')") + } + + func findDone() { + evaluate("window.__firefox__.findDone()") + } + + func findNext() { + evaluate("window.__firefox__.findNext()") + } + + func findPrevious() { + evaluate("window.__firefox__.findPrevious()") } - func hide() { - visible = false + private func evaluate(_ js: String) { + if #available(macOS 11.0, *) { + webView?.evaluateJavaScript(js, in: nil, in: WKContentWorld.defaultClient) + } else { + webView?.evaluateJavaScript(js) + } } } diff --git a/DuckDuckGo/Find In Page/FindInPageUserScript.swift b/DuckDuckGo/Find In Page/FindInPageUserScript.swift index 1e8e9f66a8..42a92b2e4b 100644 --- a/DuckDuckGo/Find In Page/FindInPageUserScript.swift +++ b/DuckDuckGo/Find In Page/FindInPageUserScript.swift @@ -37,28 +37,4 @@ final class FindInPageUserScript: NSObject, StaticUserScript { model?.update(currentSelection: currentResult, matchesFound: totalResults) } - func find(text: String, inWebView webView: WKWebView) { - evaluate(js: "window.__firefox__.find('\(text.replacingOccurrences(of: "'", with: "\\\'"))')", inWebView: webView) - } - - func done(withWebView webView: WKWebView) { - evaluate(js: "window.__firefox__.findDone()", inWebView: webView) - } - - func next(withWebView webView: WKWebView) { - evaluate(js: "window.__firefox__.findNext()", inWebView: webView) - } - - func previous(withWebView webView: WKWebView) { - evaluate(js: "window.__firefox__.findPrevious()", inWebView: webView) - } - - private func evaluate(js: String, inWebView webView: WKWebView) { - if #available(macOS 11.0, *) { - webView.evaluateJavaScript(js, in: nil, in: WKContentWorld.defaultClient) - } else { - webView.evaluateJavaScript(js) - } - } - } diff --git a/DuckDuckGo/Find In Page/FindInPageViewController.swift b/DuckDuckGo/Find In Page/FindInPageViewController.swift index a2335a6c7a..7fcb7f753f 100644 --- a/DuckDuckGo/Find In Page/FindInPageViewController.swift +++ b/DuckDuckGo/Find In Page/FindInPageViewController.swift @@ -157,7 +157,7 @@ extension FindInPageViewController { extension FindInPageViewController: NSTextFieldDelegate { func controlTextDidChange(_ obj: Notification) { - model?.update(text: textField.stringValue) + model?.find(textField.stringValue) } } diff --git a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift index 6f4ac5182b..ee93bdce7a 100644 --- a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift @@ -270,7 +270,7 @@ final class RecentlyVisitedSiteModel: ObservableObject { pages = pages.filter { !urlsToRemove.contains($0.url) } } - func fixEntities(_ contentBlocking: ContentBlocking = ContentBlocking.shared) { + func fixEntities(_ contentBlocking: AnyContentBlocking = ContentBlocking.shared) { blockedEntities = blockedEntities.filter { !$0.isEmpty }.sorted(by: { l, r in contentBlocking.prevalenceForEntity(named: l) > contentBlocking.prevalenceForEntity(named: r) }) @@ -280,7 +280,7 @@ final class RecentlyVisitedSiteModel: ObservableObject { return entityDisplayName(entityName).slugfiscated() } - func entityDisplayName(_ entityName: String, _ contentBlocking: ContentBlocking = ContentBlocking.shared) -> String { + func entityDisplayName(_ entityName: String, _ contentBlocking: AnyContentBlocking = ContentBlocking.shared) -> String { return contentBlocking.displayNameForEntity(named: entityName) } @@ -288,7 +288,7 @@ final class RecentlyVisitedSiteModel: ObservableObject { } -extension ContentBlocking { +extension ContentBlockingProtocol { func prevalenceForEntity(named entityName: String) -> Double { return trackerDataManager.trackerData.entities[entityName]?.prevalence ?? 0.0 diff --git a/DuckDuckGo/Main/View/MainViewController.swift b/DuckDuckGo/Main/View/MainViewController.swift index a6b4a874eb..b855f50699 100644 --- a/DuckDuckGo/Main/View/MainViewController.swift +++ b/DuckDuckGo/Main/View/MainViewController.swift @@ -274,10 +274,13 @@ final class MainViewController: NSViewController { } private func subscribeToFindInPage() { - let model = tabCollectionViewModel.selectedTabViewModel?.findInPage - model?.$visible.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.updateFindInPage() - }.store(in: &self.navigationalCancellables) + tabCollectionViewModel.selectedTabViewModel?.findInPage? + .$isVisible + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateFindInPage() + } + .store(in: &self.navigationalCancellables) } private func subscribeToCanGoBackForward() { @@ -296,16 +299,15 @@ final class MainViewController: NSViewController { } private func updateFindInPage() { - guard let model = tabCollectionViewModel.selectedTabViewModel?.findInPage else { findInPageViewController?.makeMeFirstResponder() os_log("MainViewController: Failed to get find in page model", type: .error) return } - findInPageContainerView.isHidden = !model.visible + findInPageContainerView.isHidden = !model.isVisible findInPageViewController?.model = model - if model.visible { + if model.isVisible { findInPageViewController?.makeMeFirstResponder() } } @@ -380,9 +382,9 @@ final class MainViewController: NSViewController { case .url, .privatePlayer: browserTabViewController.makeWebViewFirstResponder() case .preferences: - browserTabViewController.preferencesViewController.view.makeMeFirstResponder() + browserTabViewController.preferencesViewController?.view.makeMeFirstResponder() case .bookmarks: - browserTabViewController.bookmarksViewController.view.makeMeFirstResponder() + browserTabViewController.bookmarksViewController?.view.makeMeFirstResponder() case .none: shouldAdjustFirstResponderOnContentChange = true } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index e982dc4d53..8bde2243d2 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -523,7 +523,7 @@ extension MainViewController { // MARK: - Edit @IBAction func findInPage(_ sender: Any?) { - tabCollectionViewModel.selectedTabViewModel?.startFindInPage() + tabCollectionViewModel.selectedTabViewModel?.showFindInPage() } @IBAction func findInPageNext(_ sender: Any?) { diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift index 60736c481b..677d8d8f35 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift @@ -40,36 +40,45 @@ final class AddressBarButtonsViewController: NSViewController { weak var delegate: AddressBarButtonsViewControllerDelegate? - private lazy var bookmarkPopover: BookmarkPopover = { - let popover = BookmarkPopover() - popover.delegate = self - return popover - }() - - private var _permissionAuthorizationPopover: PermissionAuthorizationPopover? - private var permissionAuthorizationPopover: PermissionAuthorizationPopover { - if _permissionAuthorizationPopover == nil { - _permissionAuthorizationPopover = PermissionAuthorizationPopover() - } - return _permissionAuthorizationPopover! + private var bookmarkPopover: BookmarkPopover? + private func bookmarkPopoverCreatingIfNeeded() -> BookmarkPopover { + return bookmarkPopover ?? { + let popover = BookmarkPopover() + popover.delegate = self + self.bookmarkPopover = popover + return popover + }() + } + + private var permissionAuthorizationPopover: PermissionAuthorizationPopover? + private func permissionAuthorizationPopoverCreatingIfNeeded() -> PermissionAuthorizationPopover { + return permissionAuthorizationPopover ?? { + let popover = PermissionAuthorizationPopover() + self.permissionAuthorizationPopover = popover + return popover + }() + } + + private var popupBlockedPopover: PopupBlockedPopover? + private func popupBlockedPopoverCreatingIfNeeded() -> PopupBlockedPopover { + return popupBlockedPopover ?? { + let popover = PopupBlockedPopover() + self.popupBlockedPopover = popover + return popover + }() + } + + private var privacyDashboardPopover: PrivacyDashboardPopover? + private func privacyDashboardPopoverCreatingIfNeeded() -> PrivacyDashboardPopover { + return privacyDashboardPopover ?? { + let popover = PrivacyDashboardPopover() + popover.delegate = self + self.privacyDashboardPopover = popover + self.subscribePrivacyDashboardPendingUpdates(privacyDashboardPopover: popover) + return popover + }() } - private var _popupBlockedPopover: PopupBlockedPopover? - private var popupBlockedPopover: PopupBlockedPopover { - if _popupBlockedPopover == nil { - _popupBlockedPopover = PopupBlockedPopover() - } - return _popupBlockedPopover! - } - - private var _privacyDashboardPopover: PrivacyDashboardPopover? - private var privacyDashboardPopover: PrivacyDashboardPopover { - if _privacyDashboardPopover == nil { - _privacyDashboardPopover = PrivacyDashboardPopover() - _privacyDashboardPopover!.delegate = self - } - return _privacyDashboardPopover! - } @IBOutlet weak var privacyDashboardPositioningView: NSView! @IBOutlet weak var privacyEntryPointButton: MouseOverAnimationButton! @@ -181,7 +190,6 @@ final class AddressBarButtonsViewController: NSViewController { setupNotificationAnimationView() subscribeToSelectedTabViewModel() subscribeToBookmarkList() - subscribePrivacyDashboardPendingUpdates() subscribeToEffectiveAppearance() subscribeToIsMouseOverAnimationVisible() updateBookmarkButtonVisibility() @@ -249,10 +257,10 @@ final class AddressBarButtonsViewController: NSViewController { } @IBAction func privacyEntryPointButtonAction(_ sender: Any) { - if _permissionAuthorizationPopover?.isShown == true { + if let permissionAuthorizationPopover, permissionAuthorizationPopover.isShown { permissionAuthorizationPopover.close() } - _popupBlockedPopover?.close() + popupBlockedPopover?.close() openPrivacyDashboard() } @@ -260,7 +268,7 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } let hasEmptyAddressBar = tabCollectionViewModel.selectedTabViewModel?.addressBarString.isEmpty ?? true - let showBookmarkButton = clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover.isShown) + let showBookmarkButton = clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true) bookmarkButton.isHidden = !showBookmarkButton } @@ -271,7 +279,8 @@ final class AddressBarButtonsViewController: NSViewController { return } - if !bookmarkPopover.isShown { + let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() + if bookmarkPopover.isShown { bookmarkButton.isHidden = false bookmarkPopover.viewController.bookmark = bookmark bookmarkPopover.show(relativeTo: bookmarkButton.bounds, of: bookmarkButton, preferredEdge: .maxY) @@ -283,8 +292,12 @@ final class AddressBarButtonsViewController: NSViewController { func openPermissionAuthorizationPopover(for query: PermissionAuthorizationQuery) { let button: NSButton - var popover: NSPopover = permissionAuthorizationPopover - popover.behavior = .applicationDefined + + lazy var popover: NSPopover = { + let popover = self.permissionAuthorizationPopoverCreatingIfNeeded() + popover.behavior = .applicationDefined + return popover + }() if query.permissions.contains(.camera) || (query.permissions.contains(.microphone) && microphoneButton.isHidden && !cameraButton.isHidden) { @@ -299,7 +312,7 @@ final class AddressBarButtonsViewController: NSViewController { case .popups: guard !query.wasShownOnce else { return } button = popupsButton - popover = popupBlockedPopover + popover = popupBlockedPopoverCreatingIfNeeded() case .externalScheme: guard !query.wasShownOnce else { return } button = externalSchemeButton @@ -323,7 +336,7 @@ final class AddressBarButtonsViewController: NSViewController { func openPrivacyDashboard() { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { return } - + let privacyDashboardPopover = privacyDashboardPopoverCreatingIfNeeded() // Prevent popover from being closed with Privacy Entry Point Button, while pending updates if privacyDashboardPopover.viewController.isPendingUpdates() { return } @@ -344,9 +357,9 @@ final class AddressBarButtonsViewController: NSViewController { privacyInfoCancellable = selectedTabViewModel.tab.$privacyInfo .dropFirst() .receive(on: DispatchQueue.main) - .sink { [weak self, weak selectedTabViewModel] _ in - guard self?.privacyDashboardPopover.isShown == true, let tabViewModel = selectedTabViewModel else { return } - self?.privacyDashboardPopover.viewController.updateTabViewModel(tabViewModel) + .sink { [weak privacyDashboardPopover, weak selectedTabViewModel] _ in + guard privacyDashboardPopover?.isShown == true, let tabViewModel = selectedTabViewModel else { return } + privacyDashboardPopover?.viewController.updateTabViewModel(tabViewModel) } } @@ -602,22 +615,23 @@ final class AddressBarButtonsViewController: NSViewController { } } - private func subscribePrivacyDashboardPendingUpdates() { + private func subscribePrivacyDashboardPendingUpdates(privacyDashboardPopover: PrivacyDashboardPopover) { privacyDashboadPendingUpdatesCancellable?.cancel() + guard !AppDelegate.isRunningTests else { return } privacyDashboadPendingUpdatesCancellable = privacyDashboardPopover.viewController.rulesUpdateObserver - .$pendingUpdates.receive(on: DispatchQueue.main).sink { [weak self] _ in - let isPendingUpdate = self?.privacyDashboardPopover.viewController.isPendingUpdates() ?? false + .$pendingUpdates.dropFirst().receive(on: DispatchQueue.main).sink { [weak privacyDashboardPopover] _ in + let isPendingUpdate = privacyDashboardPopover?.viewController.isPendingUpdates() ?? false // Prevent popover from being closed when clicking away, while pending updates if isPendingUpdate { - self?.privacyDashboardPopover.behavior = .applicationDefined + privacyDashboardPopover?.behavior = .applicationDefined } else { - self?.privacyDashboardPopover.close() + privacyDashboardPopover?.close() #if DEBUG - self?.privacyDashboardPopover.behavior = .semitransient + privacyDashboardPopover?.behavior = .semitransient #else - self?.privacyDashboardPopover.behavior = .transient + privacyDashboardPopover.behavior = .transient #endif } } @@ -657,6 +671,7 @@ final class AddressBarButtonsViewController: NSViewController { for permission in selectedTabViewModel.usedPermissions.keys { guard case .requested(let query) = selectedTabViewModel.usedPermissions[permission] else { continue } + let permissionAuthorizationPopover = permissionAuthorizationPopoverCreatingIfNeeded() guard !permissionAuthorizationPopover.isShown else { if permissionAuthorizationPopover.viewController.query === query { return } permissionAuthorizationPopover.close() @@ -665,7 +680,7 @@ final class AddressBarButtonsViewController: NSViewController { openPermissionAuthorizationPopover(for: query) return } - if _permissionAuthorizationPopover?.isShown == true { + if let permissionAuthorizationPopover, permissionAuthorizationPopover.isShown { permissionAuthorizationPopover.close() } @@ -731,6 +746,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func updatePrivacyEntryPointIcon() { + guard !AppDelegate.isRunningTests else { return } guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { return } @@ -823,7 +839,7 @@ final class AddressBarButtonsViewController: NSViewController { } private func closePopover() { - privacyDashboardPopover.close() + privacyDashboardPopover?.close() } private func stopAnimations(trackerAnimations: Bool = true, @@ -951,11 +967,10 @@ extension AddressBarButtonsViewController: NSPopoverDelegate { func popoverDidClose(_ notification: Notification) { switch notification.object as? NSPopover { - case bookmarkPopover: updateBookmarkButtonVisibility() - case _privacyDashboardPopover: + case privacyDashboardPopover: privacyEntryPointButton.state = .off default: diff --git a/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift b/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift index 0e15f45ff8..82f8dbb37d 100644 --- a/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift @@ -162,7 +162,7 @@ final class MoreOptionsMenu: NSMenu { } @objc func findInPage(_ sender: NSMenuItem) { - tabCollectionViewModel.selectedTabViewModel?.findInPage.show() + tabCollectionViewModel.selectedTabViewModel?.showFindInPage() } @objc func doPrint(_ sender: NSMenuItem) { diff --git a/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift b/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift index 8cb62c6af5..30b4c7574d 100644 --- a/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift @@ -532,14 +532,13 @@ final class NavigationBarViewController: NSViewController { } private func subscribeToCredentialsToSave() { - credentialsToSaveCancellable = tabCollectionViewModel.selectedTabViewModel?.$autofillDataToSave + credentialsToSaveCancellable = tabCollectionViewModel.selectedTabViewModel?.tab.autofillDataToSavePublisher .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] in - if let data = $0 { - self?.promptToSaveAutofillData(data) - self?.tabCollectionViewModel.selectedTabViewModel?.autofillDataToSave = nil - } - }) + .sink { [weak self] data in + guard let self, let data else { return } + self.promptToSaveAutofillData(data) + self.tabCollectionViewModel.selectedTabViewModel?.tab.resetAutofillData() + } } private func promptToSaveAutofillData(_ data: AutofillData) { diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index b728d06725..0aab909c32 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -23,10 +23,10 @@ struct PreferencesSection: Hashable, Identifiable { let id: PreferencesSectionIdentifier let panes: [PreferencePaneIdentifier] - static var defaultSections: [PreferencesSection] { + static func defaultSections(includingPrivatePlayer: Bool) -> [PreferencesSection] { let regularPanes: [PreferencePaneIdentifier] = { var panes: [PreferencePaneIdentifier] = [.general, .appearance, .privacy, .autofill, .downloads] - if PrivatePlayer.shared.isAvailable { + if includingPrivatePlayer { panes.append(.privatePlayer) } return panes diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 3352f94351..1679050e8d 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -29,9 +29,9 @@ final class PreferencesSidebarModel: ObservableObject { @Published private(set) var selectedPane: PreferencePaneIdentifier = .general init( - loadSections: @autoclosure @escaping () -> [PreferencesSection] = PreferencesSection.defaultSections, - tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, - privacyConfigurationManager: PrivacyConfigurationManager = ContentBlocking.shared.privacyConfigurationManager + loadSections: @escaping () -> [PreferencesSection], + tabSwitcherTabs: [Tab.TabContent], + privacyConfigurationManager: PrivacyConfigurationManaging ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs @@ -50,6 +50,16 @@ final class PreferencesSidebarModel: ObservableObject { } } + convenience init( + tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + includePrivatePlayer: Bool + ) { + self.init(loadSections: { PreferencesSection.defaultSections(includingPrivatePlayer: includePrivatePlayer) }, + tabSwitcherTabs: tabSwitcherTabs, + privacyConfigurationManager: privacyConfigurationManager) + } + func refreshSections() { sections = loadSections() if !sections.flatMap(\.panes).contains(selectedPane), let firstPane = sections.first?.panes.first { diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 50ec436988..8377d66a15 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -24,7 +24,7 @@ final class PreferencesViewController: NSViewController { weak var delegate: BrowserTabSelectionDelegate? - let model = PreferencesSidebarModel() + let model = PreferencesSidebarModel(includePrivatePlayer: PrivatePlayer.shared.isAvailable) private var selectedTabIndexCancellable: AnyCancellable? private var selectedPreferencePaneCancellable: AnyCancellable? diff --git a/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift b/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift index 63d0845dbd..ed48448a78 100644 --- a/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift +++ b/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift @@ -35,7 +35,7 @@ final class ContentBlockingRulesUpdateObserver { self.tabViewModel = tabViewModel self.onPendingUpdates = onPendingUpdates - bindContentBlockingRulesRecompilation(publisher: ContentBlocking.shared.userContentUpdating.userContentBlockingAssets) + bindContentBlockingRulesRecompilation(publisher: (ContentBlocking.shared as? AppContentBlocking)!.userContentUpdating.userContentBlockingAssets) } public func didStartCompilation(for domain: String, token: ContentBlockerRulesManager.CompletionToken ) { diff --git a/DuckDuckGo/Smarter Encryption/PrivacyFeatures.swift b/DuckDuckGo/Smarter Encryption/PrivacyFeatures.swift index 62c02042fd..941a720274 100644 --- a/DuckDuckGo/Smarter Encryption/PrivacyFeatures.swift +++ b/DuckDuckGo/Smarter Encryption/PrivacyFeatures.swift @@ -20,9 +20,32 @@ import Foundation import BrowserServicesKit -public final class PrivacyFeatures { - - public static let httpsUpgradeStore = AppHTTPSUpgradeStore() - public static let httpsUpgrade = HTTPSUpgrade(store: httpsUpgradeStore, privacyManager: ContentBlocking.shared.privacyConfigurationManager) - +protocol PrivacyFeaturesProtocol { + var contentBlocking: AnyContentBlocking { get } + + var httpsUpgradeStore: any HTTPSUpgradeStore { get } + var httpsUpgrade: HTTPSUpgrade { get } +} +typealias AnyPrivacyFeatures = any PrivacyFeaturesProtocol + +// refactor: var PrivacyFeatures to be removed, PrivacyFeaturesProtocol to be renamed to PrivacyFeatures +// PrivacyFeatures to be passed to init methods as `some PrivacyFeatures` +// swiftlint:disable:next identifier_name +var PrivacyFeatures: AnyPrivacyFeatures { + AppPrivacyFeatures.shared +} + +final class AppPrivacyFeatures: PrivacyFeaturesProtocol { + static var shared: AnyPrivacyFeatures! + + let contentBlocking: AnyContentBlocking + let httpsUpgradeStore: any HTTPSUpgradeStore + let httpsUpgrade: HTTPSUpgrade + + init(contentBlocking: AnyContentBlocking, httpsUpgradeStore: HTTPSUpgradeStore) { + self.contentBlocking = contentBlocking + self.httpsUpgradeStore = httpsUpgradeStore + self.httpsUpgrade = HTTPSUpgrade(store: httpsUpgradeStore, privacyManager: contentBlocking.privacyConfigurationManager) + } + } diff --git a/DuckDuckGo/State Restoration/Tab+NSSecureCoding.swift b/DuckDuckGo/State Restoration/Tab+NSSecureCoding.swift index c09441daf4..b7fbbf00bf 100644 --- a/DuckDuckGo/State Restoration/Tab+NSSecureCoding.swift +++ b/DuckDuckGo/State Restoration/Tab+NSSecureCoding.swift @@ -62,6 +62,8 @@ extension Tab: NSSecureCoding { shouldLoadInBackground: false, lastSelectedAt: decoder.decodeIfPresent(at: NSSecureCodingKeys.lastSelectedAt), currentDownload: currentDownload) + + _=self.awakeAfter(using: decoder) } func encode(with coder: NSCoder) { @@ -89,6 +91,8 @@ extension Tab: NSSecureCoding { if let pane = content.preferencePane { coder.encode(pane.rawValue, forKey: NSSecureCodingKeys.preferencePane) } + + self.encodeExtensions(with: coder) } } diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index bbb08bfa00..0ac6d083f3 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -183,3 +183,9 @@ extension WindowControllersManager { } } + +extension Tab { + var isPinned: Bool { + return self.pinnedTabsManager.isTabPinned(self) + } +} diff --git a/DuckDuckGo/Windows/View/WindowsManager.swift b/DuckDuckGo/Windows/View/WindowsManager.swift index 7c6b530b13..83df1f858c 100644 --- a/DuckDuckGo/Windows/View/WindowsManager.swift +++ b/DuckDuckGo/Windows/View/WindowsManager.swift @@ -84,10 +84,8 @@ final class WindowsManager { popUp: popUp) } - class func openNewWindow(with initialUrl: URL, sourceTab: Tab? = nil) { - openNewWindow(with: Tab(content: .contentFromURL(initialUrl), - attributionState: sourceTab?.currentAttributionState, - shouldLoadInBackground: true)) + class func openNewWindow(with initialUrl: URL, parentTab: Tab? = nil) { + openNewWindow(with: Tab(content: .contentFromURL(initialUrl), parentTab: parentTab, shouldLoadInBackground: true)) } class func openNewWindow(with tabCollection: TabCollection, droppingPoint: NSPoint? = nil, contentSize: NSSize? = nil, popUp: Bool = false) { diff --git a/DuckDuckGo/Youtube Player/PrivatePlayer.swift b/DuckDuckGo/Youtube Player/PrivatePlayer.swift index cf9c949ce1..29aa319cf0 100644 --- a/DuckDuckGo/Youtube Player/PrivatePlayer.swift +++ b/DuckDuckGo/Youtube Player/PrivatePlayer.swift @@ -84,7 +84,7 @@ final class PrivatePlayer { init( preferences: PrivatePlayerPreferences = .shared, - privacyConfigurationManager: PrivacyConfigurationManaging & AnyObject = ContentBlocking.shared.privacyConfigurationManager + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager ) { self.preferences = preferences isFeatureEnabled = privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .duckPlayer) @@ -361,3 +361,29 @@ extension PrivatePlayer { return true } } + +#if DEBUG + +final class PrivatePlayerPreferencesPersistorMock: PrivatePlayerPreferencesPersistor { + var privatePlayerMode: PrivatePlayerMode + var youtubeOverlayInteracted: Bool + + init(privatePlayerMode: PrivatePlayerMode = .alwaysAsk, youtubeOverlayInteracted: Bool = false) { + self.privatePlayerMode = privatePlayerMode + self.youtubeOverlayInteracted = youtubeOverlayInteracted + } +} + +extension PrivatePlayer { + + static func mock(withMode mode: PrivatePlayerMode = .enabled) -> PrivatePlayer { + let preferencesPersistor = PrivatePlayerPreferencesPersistorMock(privatePlayerMode: mode, youtubeOverlayInteracted: true) + let preferences = PrivatePlayerPreferences(persistor: preferencesPersistor) + // runtime mock-replacement for Unit Tests, to be redone when we‘ll be doing Dependency Injection + let privacyConfigurationManager = ((NSClassFromString("MockPrivacyConfigurationManager") as? NSObject.Type)!.init() as? PrivacyConfigurationManaging)! + return PrivatePlayer(preferences: preferences, privacyConfigurationManager: privacyConfigurationManager) + } + +} + +#endif diff --git a/Integration Tests/Autoconsent/AutoconsentBackgroundTests.swift b/Integration Tests/Autoconsent/AutoconsentBackgroundTests.swift index 45090e6719..a45272e501 100644 --- a/Integration Tests/Autoconsent/AutoconsentBackgroundTests.swift +++ b/Integration Tests/Autoconsent/AutoconsentBackgroundTests.swift @@ -17,17 +17,33 @@ // import XCTest +import Common +import BrowserServicesKit +import TrackerRadarKit @testable import DuckDuckGo_Privacy_Browser @available(macOS 11, *) class AutoconsentBackgroundTests: XCTestCase { + // todo: mock + let preferences = PrivacySecurityPreferences.shared + func testUserscriptIntegration() { // enable the feature let prefs = PrivacySecurityPreferences.shared prefs.autoconsentEnabled = true // setup a webview with autoconsent userscript installed - let autoconsentUserScript = AutoconsentUserScript(scriptSource: DefaultScriptSourceProvider(), - config: ContentBlocking.shared.privacyConfigurationManager.privacyConfig) + let sourceProvider = ScriptSourceProvider(configStorage: MockStorage(), + privacyConfigurationManager: MockPrivacyConfigurationManager(), + privacySettings: preferences, + contentBlockingManager: ContentBlockerRulesManagerMock(), + trackerDataManager: TrackerDataManager(etag: DefaultConfigurationStorage.shared.loadEtag(for: .trackerRadar), + data: DefaultConfigurationStorage.shared.loadData(for: .trackerRadar), + embeddedDataProvider: AppTrackerDataSetProvider(), + errorReporting: nil), + + tld: TLD()) + let autoconsentUserScript = AutoconsentUserScript(scriptSource: sourceProvider, + config: MockPrivacyConfigurationManager().privacyConfig) let configuration = WKWebViewConfiguration() configuration.userContentController.addUserScript(autoconsentUserScript.makeWKUserScript()) @@ -57,5 +73,49 @@ class AutoconsentBackgroundTests: XCTestCase { } waitForExpectations(timeout: 4) } +} + +class MockStorage: ConfigurationStoring { + + enum Error: Swift.Error { + case mockError + } + + var errorOnStoreData = false + var errorOnStoreEtag = false + + var data: Data? + var dataConfig: ConfigurationLocation? + + var etag: String? + var etagConfig: ConfigurationLocation? + + func loadData(for: ConfigurationLocation) -> Data? { + return data + } + + func loadEtag(for: ConfigurationLocation) -> String? { + return etag + } + + func saveData(_ data: Data, for config: ConfigurationLocation) throws { + if errorOnStoreData { + throw Error.mockError + } + + self.data = data + self.dataConfig = config + } + + func saveEtag(_ etag: String, for config: ConfigurationLocation) throws { + if errorOnStoreEtag { + throw Error.mockError + } + + self.etag = etag + self.etagConfig = config + } + + func log() { } } diff --git a/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift b/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift index 12731ab109..0be0de4bc3 100644 --- a/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift +++ b/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift @@ -16,14 +16,26 @@ // limitations under the License. // +import BrowserServicesKit +import Common import XCTest @testable import DuckDuckGo_Privacy_Browser @available(macOS 11, *) class AutoconsentMessageProtocolTests: XCTestCase { + let userScript = AutoconsentUserScript( - scriptSource: DefaultScriptSourceProvider(), - config: ContentBlocking.shared.privacyConfigurationManager.privacyConfig + scriptSource: ScriptSourceProvider(configStorage: ConfigurationDownloaderTests.MockStorage(), + privacyConfigurationManager: MockPrivacyConfigurationManager(), + privacySettings: PrivacySecurityPreferences.shared, // todo: mock + contentBlockingManager: ContentBlockerRulesManagerMock(), + trackerDataManager: TrackerDataManager(etag: DefaultConfigurationStorage.shared.loadEtag(for: .trackerRadar), + data: DefaultConfigurationStorage.shared.loadData(for: .trackerRadar), + embeddedDataProvider: AppTrackerDataSetProvider(), + errorReporting: nil), + + tld: TLD()), + config: MockPrivacyConfiguration() ) override func setUp() { @@ -99,7 +111,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { }, message: message ) - waitForExpectations(timeout: 1.0) + waitForExpectations(timeout: 5.0) } @MainActor diff --git a/Unit Tests/Content Blocker/ContentBlockerRulesManagerMock.swift b/Unit Tests/Content Blocker/ContentBlockerRulesManagerMock.swift index 84330c378c..2bc032b231 100644 --- a/Unit Tests/Content Blocker/ContentBlockerRulesManagerMock.swift +++ b/Unit Tests/Content Blocker/ContentBlockerRulesManagerMock.swift @@ -20,7 +20,15 @@ import BrowserServicesKit import Combine -final class ContentBlockerRulesManagerMock: ContentBlockerRulesManagerProtocol { +@objc(ContentBlockerRulesManagerMock) +final class ContentBlockerRulesManagerMock: NSObject, ContentBlockerRulesManagerProtocol { + func scheduleCompilation() -> BrowserServicesKit.ContentBlockerRulesManager.CompletionToken { + fatalError() + } + + var currentMainRules: BrowserServicesKit.ContentBlockerRulesManager.Rules? + + var currentAttributionRules: BrowserServicesKit.ContentBlockerRulesManager.Rules? var updatesPublisher: AnyPublisher { updatesSubject.eraseToAnyPublisher() diff --git a/Unit Tests/Content Blocker/ContentBlockingMock.swift b/Unit Tests/Content Blocker/ContentBlockingMock.swift new file mode 100644 index 0000000000..fbc733192b --- /dev/null +++ b/Unit Tests/Content Blocker/ContentBlockingMock.swift @@ -0,0 +1,115 @@ +// +// ContentBlockingMock.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 BrowserServicesKit +import Combine +import Common +import Foundation +@testable import DuckDuckGo_Privacy_Browser + +@objc(ContentBlockingMock) +final class ContentBlockingMock: NSObject, ContentBlockingProtocol, AdClickAttributionDependencies { + + struct EDP: EmbeddedDataProvider { + var embeddedDataEtag: String = "" + var embeddedData: Data = .init() + } + var trackerDataManager = TrackerDataManager(etag: nil, data: nil, embeddedDataProvider: EDP(), errorReporting: nil) + + var tld: Common.TLD = .init() + + typealias ConfigurationManager = MockPrivacyConfigurationManager + typealias ContentBlockingAssets = UserContentUpdating.NewContent + + typealias ContentBlockingManager = ContentBlockerRulesManagerMock + + let contentBlockingManager: ContentBlockerRulesManagerProtocol = ContentBlockerRulesManagerMock() + let contentBlockingAssetsPublisher = PassthroughSubject().eraseToAnyPublisher() + let contentBlockerRulesManager = ContentBlockerRulesManagerMock() + let privacyConfigurationManager: PrivacyConfigurationManaging = MockPrivacyConfigurationManager() + + var adClickAttribution: AdClickAttributing = MockAttributing() + var adClickAttributionRulesProvider: AdClickAttributionRulesProviding = MockAttributionRulesProvider() + + var attributionEvents: EventMapping? + var attributionDebugEvents: BrowserServicesKit.EventMapping? + +} + +@objc(HTTPSUpgradeStoreMock) +final class HTTPSUpgradeStoreMock: NSObject, HTTPSUpgradeStore { + + var bloomFilter: BloomFilterWrapper? + var bloomFilterSpecification: HTTPSBloomFilterSpecification? + + var excludedDomains: [String] = [] + func hasExcludedDomain(_ domain: String) -> Bool { + excludedDomains.contains(domain) + } + +} + +final class MockAttributing: AdClickAttributing { + + init(onFormatMatching: @escaping (URL) -> Bool = { _ in return true }, + onParameterNameQuery: @escaping (URL) -> String? = { _ in return nil }) { + self.onFormatMatching = onFormatMatching + self.onParameterNameQuery = onParameterNameQuery + } + + var isEnabled = true + + var allowlist = [AdClickAttributionFeature.AllowlistEntry]() + + var navigationExpiration: Double = 30 + var totalExpiration: Double = 7 * 24 * 60 + + var onFormatMatching: (URL) -> Bool + var onParameterNameQuery: (URL) -> String? + + func isMatchingAttributionFormat(_ url: URL) -> Bool { + return onFormatMatching(url) + } + + func attributionDomainParameterName(for url: URL) -> String? { + return onParameterNameQuery(url) + } + + var isHeuristicDetectionEnabled: Bool = true + var isDomainDetectionEnabled: Bool = true + +} + +final class MockAttributionRulesProvider: AdClickAttributionRulesProviding { + + enum Constants { + static let globalAttributionRulesListName = "global" + } + + init() { + } + + var globalAttributionRules: ContentBlockerRulesManager.Rules? + + var onRequestingAttribution: (String, @escaping (ContentBlockerRulesManager.Rules?) -> Void) -> Void = { _, _ in } + func requestAttribution(forVendor vendor: String, + completion: @escaping (ContentBlockerRulesManager.Rules?) -> Void) { + onRequestingAttribution(vendor, completion) + } + +} diff --git a/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift b/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift index 382c6e732a..57c9cdceab 100644 --- a/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift +++ b/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift @@ -18,17 +18,27 @@ import XCTest import WebKit +import Common import TrackerRadarKit import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser class ContentBlockingUpdatingTests: XCTestCase { + // todo: mock let preferences = PrivacySecurityPreferences.shared let rulesManager = ContentBlockerRulesManagerMock() var updating: UserContentUpdating! override func setUp() { - updating = UserContentUpdating(contentBlockerRulesManager: rulesManager, privacySecurityPreferences: preferences) + updating = UserContentUpdating(contentBlockerRulesManager: rulesManager, + privacyConfigurationManager: MockPrivacyConfigurationManager(), + trackerDataManager: TrackerDataManager(etag: DefaultConfigurationStorage.shared.loadEtag(for: .trackerRadar), + data: DefaultConfigurationStorage.shared.loadData(for: .trackerRadar), + embeddedDataProvider: AppTrackerDataSetProvider(), + errorReporting: nil), + configStorage: ConfigurationDownloaderTests.MockStorage(), + privacySecurityPreferences: preferences, + tld: TLD()) } override static func setUp() { diff --git a/Unit Tests/Content Blocker/EmptyAttributionRulesProver.swift b/Unit Tests/Content Blocker/EmptyAttributionRulesProver.swift new file mode 100644 index 0000000000..8109e26467 --- /dev/null +++ b/Unit Tests/Content Blocker/EmptyAttributionRulesProver.swift @@ -0,0 +1,31 @@ +// +// EmptyAttributionRulesProver.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 +import TrackerRadarKit +import BrowserServicesKit + +@objc(EmptyAttributionRulesProver) +final class EmptyAttributionRulesProver: NSObject, AdClickAttributionRulesProviding { + var globalAttributionRules: BrowserServicesKit.ContentBlockerRulesManager.Rules? + + func requestAttribution(forVendor vendor: String, completion: @escaping (BrowserServicesKit.ContentBlockerRulesManager.Rules?) -> Void) { + completion(nil) + } + +} diff --git a/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift b/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift index e6a37c6f22..47a2400fa6 100644 --- a/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift +++ b/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift @@ -21,6 +21,11 @@ import XCTest class RecentlyVisitedSiteModelTests: XCTestCase { + // swiftlint:disable:next identifier_name + private func RecentlyVisitedSiteModel(originalURL: URL, privatePlayer: PrivatePlayerMode = .disabled) -> HomePage.Models.RecentlyVisitedSiteModel? { + HomePage.Models.RecentlyVisitedSiteModel(originalURL: originalURL, bookmarkManager: LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(), faviconManagement: FaviconManagerMock()), fireproofDomains: FireproofDomains(store: FireproofDomainsStoreMock()), privatePlayer: .mock(withMode: privatePlayer)) + } + func testWhenOriginalURLIsHTTPS_ThenModelURLIsHTTPS() { assertModelWithURL(URL(string: "https://example.com")!, matches: URL(string: "https://example.com")!, expectedDomain: "example.com") } @@ -39,34 +44,22 @@ class RecentlyVisitedSiteModelTests: XCTestCase { } func testWhenPrivatePlayerIsEnabled_ThenPrivatePlayerURLSetsDomainPlaceholder() { - let model = HomePage.Models.RecentlyVisitedSiteModel( - originalURL: .effectivePrivatePlayer("abcde12345"), - privatePlayer: .mock(withMode: .enabled) - ) + let model = RecentlyVisitedSiteModel(originalURL: .effectivePrivatePlayer("abcde12345"), privatePlayer: .enabled) XCTAssertEqual(model?.isRealDomain, false) XCTAssertEqual(model?.domainToDisplay, PrivatePlayer.commonName) } func testWhenPrivatePlayerIsDisabled_ThenPrivatePlayerURLDoesNotSetDomainPlaceholder() { let url = URL.effectivePrivatePlayer("abcde12345") - let model = HomePage.Models.RecentlyVisitedSiteModel(originalURL: url, privatePlayer: .mock(withMode: .disabled)) + let model = RecentlyVisitedSiteModel(originalURL: url) XCTAssertEqual(model?.isRealDomain, true) XCTAssertEqual(model?.domainToDisplay, model?.domain) } private func assertModelWithURL(_ url: URL, matches expectedURL: URL, expectedDomain: String) { - let model = HomePage.Models.RecentlyVisitedSiteModel(originalURL: url) + let model = RecentlyVisitedSiteModel(originalURL: url) XCTAssertEqual(model?.isRealDomain, true) XCTAssertEqual(model?.domain, expectedDomain) XCTAssertEqual(model?.url, expectedURL) } } - -private extension PrivatePlayer { - - static func mock(withMode mode: PrivatePlayerMode = .enabled) -> PrivatePlayer { - let preferencesPersistor = PrivatePlayerPreferencesPersistorMock(privatePlayerMode: mode, youtubeOverlayInteracted: true) - let preferences = PrivatePlayerPreferences(persistor: preferencesPersistor) - return PrivatePlayer(preferences: preferences) - } -} diff --git a/Unit Tests/Preferences/PreferencesSidebarModelTests.swift b/Unit Tests/Preferences/PreferencesSidebarModelTests.swift index ca8fe81500..ee4a2de55c 100644 --- a/Unit Tests/Preferences/PreferencesSidebarModelTests.swift +++ b/Unit Tests/Preferences/PreferencesSidebarModelTests.swift @@ -29,6 +29,11 @@ final class PreferencesSidebarModelTests: XCTestCase { cancellables.removeAll() } + // swiftlint:disable:next identifier_name + private func PreferencesSidebarModel(loadSections: [PreferencesSection]? = nil, tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes) -> DuckDuckGo_Privacy_Browser.PreferencesSidebarModel { + return DuckDuckGo_Privacy_Browser.PreferencesSidebarModel(loadSections: { loadSections ?? PreferencesSection.defaultSections(includingPrivatePlayer: false) }, tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: MockPrivacyConfigurationManager()) + } + func testWhenInitializedThenFirstPaneInFirstSectionIsSelected() throws { let sections: [PreferencesSection] = [.init(id: .regularPreferencePanes, panes: [.appearance, .downloads, .autofill])] let model = PreferencesSidebarModel(loadSections: sections) diff --git a/Unit Tests/Preferences/PrivatePlayerPreferencesTests.swift b/Unit Tests/Preferences/PrivatePlayerPreferencesTests.swift index 3b63726e85..6f9d596af3 100644 --- a/Unit Tests/Preferences/PrivatePlayerPreferencesTests.swift +++ b/Unit Tests/Preferences/PrivatePlayerPreferencesTests.swift @@ -20,16 +20,6 @@ import Foundation import XCTest @testable import DuckDuckGo_Privacy_Browser -final class PrivatePlayerPreferencesPersistorMock: PrivatePlayerPreferencesPersistor { - var privatePlayerMode: PrivatePlayerMode - var youtubeOverlayInteracted: Bool - - init(privatePlayerMode: PrivatePlayerMode = .alwaysAsk, youtubeOverlayInteracted: Bool = false) { - self.privatePlayerMode = privatePlayerMode - self.youtubeOverlayInteracted = youtubeOverlayInteracted - } -} - final class PrivatePlayerPreferencesTests: XCTestCase { func testWhenInitializedThenItLoadsPersistedValues() throws { diff --git a/Unit Tests/Youtube Player/PrivatePlayerTests.swift b/Unit Tests/Youtube Player/PrivatePlayerTests.swift index 16b042f967..6d55118a41 100644 --- a/Unit Tests/Youtube Player/PrivatePlayerTests.swift +++ b/Unit Tests/Youtube Player/PrivatePlayerTests.swift @@ -21,7 +21,7 @@ import BrowserServicesKit import Combine @testable import DuckDuckGo_Privacy_Browser -private class MockPrivacyConfiguration: PrivacyConfiguration { +class MockPrivacyConfiguration: PrivacyConfiguration { var identifier: String = "MockPrivacyConfiguration" var userUnprotectedDomains: [String] = [] var tempUnprotectedDomains: [String] = [] @@ -39,7 +39,8 @@ private class MockPrivacyConfiguration: PrivacyConfiguration { func userDisabledProtection(forDomain: String) {} } -private class MockPrivacyConfigurationManager: PrivacyConfigurationManaging { +@objc(MockPrivacyConfigurationManager) +class MockPrivacyConfigurationManager: NSObject, PrivacyConfigurationManaging { var embeddedConfigData: BrowserServicesKit.PrivacyConfigurationManager.ConfigurationData { fatalError("not implemented") } From b186b58a0629649255a710a541f65f1511381af4 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 14 Dec 2022 13:59:40 +0600 Subject: [PATCH 02/29] fix Release build (#896) --- DuckDuckGo/App Delegate/AppDelegate.swift | 3 ++- DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift | 3 +++ .../View/AddressBarButtonsViewController.swift | 2 +- DuckDuckGo/Youtube Player/PrivatePlayer.swift | 6 ++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/App Delegate/AppDelegate.swift b/DuckDuckGo/App Delegate/AppDelegate.swift index be1faa1927..927453cd3e 100644 --- a/DuckDuckGo/App Delegate/AppDelegate.swift +++ b/DuckDuckGo/App Delegate/AppDelegate.swift @@ -86,7 +86,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { ? AppPrivacyFeatures(contentBlocking: mock("ContentBlockingMock"), httpsUpgradeStore: mock("HTTPSUpgradeStoreMock")) : AppPrivacyFeatures(contentBlocking: AppContentBlocking(), httpsUpgradeStore: AppHTTPSUpgradeStore()) #else - PrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking()) + AppPrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking(), + httpsUpgradeStore: AppHTTPSUpgradeStore()) #endif do { diff --git a/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift b/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift index e431422df2..547820f2f1 100644 --- a/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift +++ b/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift @@ -45,9 +45,12 @@ extension TabExtensionsBuilder { struct AppTabExtensions: TabExtensionsBuilder { var components = [any TabExtension]() } + +#if DEBUG struct TestTabExtensions: TabExtensionsBuilder { var components = [any TabExtension]() } +#endif struct TabExtensions { typealias ExtensionType = TabExtension diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift index 677d8d8f35..a2c9e457c1 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift @@ -631,7 +631,7 @@ final class AddressBarButtonsViewController: NSViewController { #if DEBUG privacyDashboardPopover?.behavior = .semitransient #else - privacyDashboardPopover.behavior = .transient + privacyDashboardPopover?.behavior = .transient #endif } } diff --git a/DuckDuckGo/Youtube Player/PrivatePlayer.swift b/DuckDuckGo/Youtube Player/PrivatePlayer.swift index 29aa319cf0..6f68e612a7 100644 --- a/DuckDuckGo/Youtube Player/PrivatePlayer.swift +++ b/DuckDuckGo/Youtube Player/PrivatePlayer.swift @@ -386,4 +386,10 @@ extension PrivatePlayer { } +#else + +extension PrivatePlayer { + static func mock(withMode mode: PrivatePlayerMode = .enabled) -> PrivatePlayer { fatalError() } +} + #endif From 0548cdd97cfa61ce70fd116a05f52d532755cf03 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 14 Dec 2022 17:45:57 +0600 Subject: [PATCH 03/29] Improve Tab Extensions testability, fix AdClick Attribution inherited attribution argument passing (#897) --- .../AdClickAttributionTabExtension.swift | 3 + .../Extensions/TabExtensions.swift | 59 ++++-- DuckDuckGo/Browser Tab/Model/Tab.swift | 27 ++- .../Model/TabExtensionsBuilder.swift | 191 +++++++++++++++--- 4 files changed, 224 insertions(+), 56 deletions(-) diff --git a/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift index 32033826b2..46261b59f6 100644 --- a/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift +++ b/DuckDuckGo/Browser Tab/Extensions/AdClickAttributionTabExtension.swift @@ -166,6 +166,9 @@ extension AdClickAttributionTabExtension: AdClickAttributionLogicDelegate { extension AppContentBlocking: AdClickAttributionDependencies {} protocol AdClickAttributionProtocol { + var currentAttributionState: AdClickAttributionLogic.State? { get } + + // to be removed var detection: AdClickAttributionDetection! { get } var logic: AdClickAttributionLogic! { get } } diff --git a/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift b/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift index a2df797b10..36745e24de 100644 --- a/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift +++ b/DuckDuckGo/Browser Tab/Extensions/TabExtensions.swift @@ -58,16 +58,20 @@ protocol NSCodingExtension: TabExtension { // Define dependencies used to instantiate TabExtensions here: protocol TabExtensionDependencies { - var userScriptsPublisher: AnyPublisher { get } - var contentBlocking: ContentBlockingProtocol { get } - var adClickAttributionDependencies: AdClickAttributionDependencies { get } - var privacyInfoPublisher: AnyPublisher { get } - - var inheritedAttribution: AdClickAttributionLogic.State? { get } - var userContentControllerProvider: UserContentControllerProvider { get } + var privacyFeatures: PrivacyFeaturesProtocol { get } + var historyCoordinating: HistoryCoordinating { get } } - -extension AppTabExtensions { +// swiftlint:disable:next large_tuple +typealias TabExtensionsBuilderArguments = ( + tabIdentifier: UInt64, + userScriptsPublisher: AnyPublisher, + inheritedAttribution: AdClickAttributionLogic.State?, + userContentControllerProvider: UserContentControllerProvider, + permissionModel: PermissionModel, + privacyInfoPublisher: AnyPublisher +) + +extension TabExtensionsBuilder { /// Instantiate `TabExtension`-s for App builds here /// use add { return SomeTabExtensions() } to register Tab Extensions @@ -76,10 +80,10 @@ extension AppTabExtensions { /// ` let myPublishingExtension = add { MyPublishingExtension() } /// ` add { MyOtherExtension(with: myExtension.resultPublisher) } /// Note: Extensions with state restoration support should conform to `NSCodingExtension` - mutating func make(with dependencies: TabExtensionDependencies) { - let userScripts = dependencies.userScriptsPublisher + mutating func registerExtensions(with args: TabExtensionsBuilderArguments, dependencies: TabExtensionDependencies) { + let userScripts = args.userScriptsPublisher - let trackerInfoPublisher = dependencies.privacyInfoPublisher + let trackerInfoPublisher = args.privacyInfoPublisher .compactMap { $0?.$trackerInfo } .switchToLatest() .scan( (old: Set(), new: Set()) ) { @@ -91,11 +95,11 @@ extension AppTabExtensions { .switchToLatest() add { - AdClickAttributionTabExtension(inheritedAttribution: dependencies.inheritedAttribution, - userContentControllerProvider: dependencies.userContentControllerProvider, + AdClickAttributionTabExtension(inheritedAttribution: args.inheritedAttribution, + userContentControllerProvider: args.userContentControllerProvider, contentBlockerRulesScriptPublisher: userScripts.map(\.?.contentBlockerRulesScript), trackerInfoPublisher: trackerInfoPublisher, - dependencies: dependencies.adClickAttributionDependencies) + dependencies: dependencies.privacyFeatures.contentBlocking) } add { AutofillTabExtension(autofillUserScriptPublisher: userScripts.map(\.?.autofillScript)) @@ -114,11 +118,26 @@ extension AppTabExtensions { } #if DEBUG -extension TestTabExtensions { - - /// Add `TabExtension`-s that should be loaded when running Unit Tests here - /// By default the Extensions won‘t be loaded - mutating func make(with dependencies: TabExtensionDependencies) { +extension TestTabExtensionsBuilder { + + /// Used by default for Tab instantiation if not provided in Tab(... extensionsBuilder: TestTabExtensionsBuilder([HistoryTabExtension.self]) + static var `default` = TestTabExtensionsBuilder(overrideExtensions: TestTabExtensionsBuilder.overrideExtensions, [ + // FindInPageTabExtension.self, HistoryTabExtension.self, ... - add TabExtensions here to be loaded by default for ALL Unit Tests + ]) + + // override Tab Extensions initialisation registered in TabExtensionsBuilder.registerExtensions for Unit Tests + func overrideExtensions(with args: TabExtensionsBuilderArguments, dependencies: TabExtensionDependencies) { + /** ``` + let fbProtection = get(FBProtectionTabExtension.self) + + let contentBlocking = override { + ContentBlockingTabExtension(fbBlockingEnabledProvider: fbProtection.value) + } + override { + HistoryTabExtension(trackersPublisher: contentBlocking.trackersPublisher) + } + ... + */ } diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index d27a5c7fd1..81a57b0e51 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -147,12 +147,8 @@ final class Tab: NSObject, Identifiable, ObservableObject { } } private struct ExtensionDependencies: TabExtensionDependencies { - var userScriptsPublisher: AnyPublisher - var contentBlocking: ContentBlockingProtocol - var adClickAttributionDependencies: AdClickAttributionDependencies - var privacyInfoPublisher: AnyPublisher - var inheritedAttribution: BrowserServicesKit.AdClickAttributionLogic.State? - var userContentControllerProvider: UserContentControllerProvider + let privacyFeatures: PrivacyFeaturesProtocol + let historyCoordinating: HistoryCoordinating } // "protected" delegate property for extensions usage @@ -187,6 +183,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { pinnedTabsManager: PinnedTabsManager = WindowControllersManager.shared.pinnedTabsManager, privatePlayer: PrivatePlayer? = nil, cbaTimeReporter: ContentBlockingAssetsCompilationTimeReporter? = ContentBlockingAssetsCompilationTimeReporter.shared, + extensionsBuilder: TabExtensionsBuilderProtocol = TabExtensionsBuilder.default, localHistory: Set = Set(), title: String? = nil, error: Error? = nil, @@ -212,6 +209,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { pinnedTabsManager: pinnedTabsManager, privacyFeatures: PrivacyFeatures, privatePlayer: privatePlayer, + extensionsBuilder: extensionsBuilder, cbaTimeReporter: cbaTimeReporter, localHistory: localHistory, title: title, @@ -227,6 +225,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { webViewFrame: webViewFrame) } + // swiftlint:disable:next function_body_length init(content: TabContent, faviconManagement: FaviconManagement, webCacheManager: WebCacheManager, @@ -235,6 +234,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { pinnedTabsManager: PinnedTabsManager, privacyFeatures: some PrivacyFeaturesProtocol, privatePlayer: PrivatePlayer, + extensionsBuilder: TabExtensionsBuilderProtocol, cbaTimeReporter: ContentBlockingAssetsCompilationTimeReporter?, localHistory: Set, title: String?, @@ -286,11 +286,16 @@ final class Tab: NSObject, Identifiable, ObservableObject { .eraseToAnyPublisher() var userContentControllerProvider: UserContentControllerProvider? - self.extensions = .builder().build(with: ExtensionDependencies(userScriptsPublisher: userScriptsPublisher, - contentBlocking: privacyFeatures.contentBlocking, - adClickAttributionDependencies: privacyFeatures.contentBlocking, - privacyInfoPublisher: _privacyInfo.projectedValue.eraseToAnyPublisher(), - userContentControllerProvider: { userContentControllerProvider?() })) + self.extensions = extensionsBuilder + .build(with: (tabIdentifier: instrumentation.currentTabIdentifier, + userScriptsPublisher: userScriptsPublisher, + inheritedAttribution: parentTab?.adClickAttribution?.currentAttributionState, + userContentControllerProvider: { userContentControllerProvider?() }, + permissionModel: permissions, + privacyInfoPublisher: _privacyInfo.projectedValue.eraseToAnyPublisher() + ), + dependencies: ExtensionDependencies(privacyFeatures: privacyFeatures, + historyCoordinating: historyCoordinating)) super.init() userContentControllerProvider = { [weak self] in self?.userContentController } diff --git a/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift b/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift index 547820f2f1..1f2b05eeb3 100644 --- a/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift +++ b/DuckDuckGo/Browser Tab/Model/TabExtensionsBuilder.swift @@ -18,37 +18,180 @@ import Combine import Foundation +import os.log -protocol TabExtensionsBuilder { - var components: [any TabExtension] { get set } - mutating func make(with dependencies: TabExtensionDependencies) - func build(with dependencies: TabExtensionDependencies) -> TabExtensions +protocol TabExtensionsBuilderProtocol { + func build(with args: TabExtensionsBuilderArguments, dependencies: TabExtensionDependencies) -> TabExtensions } -extension TabExtensionsBuilder { +// !! Register Tab Extensions in TabExtensions.swift +/// Tab Extensions registration component +/// defines intialization order and provides dependencies to the Tab Extensions initalizers +struct TabExtensionsBuilder: TabExtensionsBuilderProtocol { + + static var `default`: TabExtensionsBuilderProtocol { +#if DEBUG + return AppDelegate.isRunningTests ? TestTabExtensionsBuilder.default : TabExtensionsBuilder() +#else + return TabExtensionsBuilder() +#endif + } + + var components = [(type: any TabExtension.Type, buildingBlock: AnyTabExtensionBuildingBlock)]() + + /// collect Tab Extensions instantiation blocks (`add { }` method calls) + /// lazy for Unit Tests builds and non-lazy in Production @discardableResult - mutating func add(_ makeTabExtension: () -> T) -> T { - let tabExtension = makeTabExtension() - components.append(tabExtension) - return tabExtension + mutating func add(_ makeTabExtension: @escaping () -> Extension) -> TabExtensionBuildingBlock { + let buildingBlock = TabExtensionBuildingBlock(makeTabExtension) + components.append( (type: Extension.self, buildingBlock: buildingBlock) ) + return buildingBlock } - func build(with dependencies: TabExtensionDependencies) -> TabExtensions { + /// build TabExtensions struct from blocks collected above + func build(with args: TabExtensionsBuilderArguments, dependencies: TabExtensionDependencies) -> TabExtensions { var builder = self - builder.make(with: dependencies) - return TabExtensions(components: builder.components) + builder.registerExtensions(with: args, dependencies: dependencies) + return TabExtensions(components: builder.components.map { $0.buildingBlock.make() }) } } -struct AppTabExtensions: TabExtensionsBuilder { - var components = [any TabExtension]() +#if DEBUG +/// TabExtensionsBuilder loaded by default when running Tests +/// by default loads only extensions passed in `load` argument, +/// set default extensions to load in TestTabExtensionsBuilder.default +/// provide overriding extensions initializers in `overrideExtensions` method using `override { .. }` calls +final class TestTabExtensionsBuilder: TabExtensionsBuilderProtocol { + private var components = [(type: any TabExtension.Type, buildingBlock: AnyTabExtensionBuildingBlock)]() + + var extensionsToLoad = [any TabExtension.Type]() + private let overrideExtensionsFunc: (TestTabExtensionsBuilder) -> (TabExtensionsBuilderArguments, TabExtensionDependencies) -> Void + + init(load extensionsToLoad: [any TabExtension.Type], + overrideExtensions: @escaping (TestTabExtensionsBuilder) -> (TabExtensionsBuilderArguments, TabExtensionDependencies) -> Void = TestTabExtensionsBuilder.overrideExtensions) { + self.extensionsToLoad = extensionsToLoad + self.overrideExtensionsFunc = overrideExtensions + } + + convenience init(overrideExtensions: @escaping (TestTabExtensionsBuilder) -> (TabExtensionsBuilderArguments, TabExtensionDependencies) -> Void, + _ extensionsToLoad: [any TabExtension.Type]) { + self.init(load: extensionsToLoad, overrideExtensions: overrideExtensions) + } + + convenience init(load extensionToLoad: any TabExtension.Type, + overrideExtensions: @escaping (TestTabExtensionsBuilder) -> (TabExtensionsBuilderArguments, TabExtensionDependencies) -> Void = TestTabExtensionsBuilder.overrideExtensions) { + self.init(load: [extensionToLoad], overrideExtensions: overrideExtensions) + } + + func build(with args: TabExtensionsBuilderArguments, dependencies: TabExtensionDependencies) -> TabExtensions { + var builder = TabExtensionsBuilder() + builder.registerExtensions(with: args, dependencies: dependencies) + + self.components = builder.components.filter { component in extensionsToLoad.contains(where: { component.type == $0 }) } + self.overrideExtensionsFunc(self)(args, dependencies) + + return TabExtensions(components: components.map { $0.buildingBlock.make() }) + } + + /// collect Tab Extensions instantiation blocks (`add { }` method calls) + /// lazy for Unit Tests builds and non-lazy in Production + @discardableResult + func override(_ makeTabExtension: @escaping () -> Extension) -> TabExtensionBuildingBlock { + let builderBlock = TabExtensionBuildingBlock(makeTabExtension) + guard let idx = components.firstIndex(where: { $0.type == Extension.self }) else { + return TabExtensionBuildingBlock { + fatalError("Trying to initialize an extension not specified in TestTabExtensionsBuilder.extensionsToLoad: \(Extension.self)") + } + } + guard case .lazy(let loader) = (components[idx].buildingBlock as? TabExtensionBuildingBlock)?.state, + case .none = loader.state + else { + fatalError("\(Extension.self) has been already initialized at the moment of the `override` call") + } + loader.state = .none(makeTabExtension) + + return builderBlock + } + + /// use to retreive Extension Building Blocks registered during TabExtensionsBuilder.registerExtensions + func get(_: Extension.Type) -> TabExtensionBuildingBlock { + guard let buildingBlock = components.first(where: { $0.type == Extension.self })?.buildingBlock else { + fatalError("\(Extension.self) not registered in TabExtensionsBuilder.registerExtensions") + } + return (buildingBlock as? TabExtensionBuildingBlock)! + } + +} +#endif + +@dynamicMemberLookup +struct TabExtensionBuildingBlock { +#if DEBUG + + fileprivate enum State { + case loaded(Extension) + case lazy(TabExtensionLazyLoader) + } + fileprivate let state: State + var value: Extension { + switch state { + case .loaded(let value): return value + case .lazy(let loader): return loader.value + } + } + + init(_ makeTabExtension: @escaping () -> Extension) { + if AppDelegate.isRunningTests { + state = .lazy(.init(makeTabExtension)) + } else { + state = .loaded(makeTabExtension()) + } + } + +#else + + let value: Extension + + init(_ makeTabExtension: @escaping () -> Extension) { + self.value = makeTabExtension() + } + +#endif + + subscript(dynamicMember keyPath: KeyPath) -> T { + self.value[keyPath: keyPath] + } +} +protocol AnyTabExtensionBuildingBlock { + func make() -> any TabExtension +} +extension TabExtensionBuildingBlock: AnyTabExtensionBuildingBlock { + func make() -> any TabExtension { + self.value + } } #if DEBUG -struct TestTabExtensions: TabExtensionsBuilder { - var components = [any TabExtension]() +private final class TabExtensionLazyLoader { + fileprivate enum State { + case none(() -> Extension) + case some(Extension) + } + fileprivate var state: State + + var value: Extension { + if case .none(let makeTabExtension) = state { + state = .some(makeTabExtension()) + } + guard case .some(let value) = state else { fatalError() } + return value + } + + init(_ makeTabExtension: @escaping () -> Extension) { + self.state = .none(makeTabExtension) + } } #endif @@ -57,14 +200,6 @@ struct TabExtensions { private(set) var extensions: [AnyKeyPath: any TabExtension] - static func builder() -> TabExtensionsBuilder { -#if DEBUG - return AppDelegate.isRunningTests ? TestTabExtensions() : AppTabExtensions() -#else - return AppTabExtensions() -#endif - } - init(components: [any TabExtension]) { var extensions = [AnyKeyPath: any TabExtension]() func add(_ tabExtension: T) { @@ -76,7 +211,13 @@ struct TabExtensions { } func resolve(_: T.Type) -> T.PublicProtocol? { - (extensions[\T.self] as? T)?.getPublicProtocol() + let tabExtension = (extensions[\T.self] as? T)?.getPublicProtocol() +#if DEBUG + assert(AppDelegate.isRunningTests || tabExtension != nil) +#else + os_log("%s Tab Extension not initialised for Unit Tests, activate it in TabExtensions.swift", log: .autoconsent, type: .debug, "\(T.self)") +#endif + return tabExtension } func resolve(_: T.Type) -> T.PublicProtocol? where T.PublicProtocol == T { From 28ee1485cfdfd2ef716bf0324bb6b1e53a318b1d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 14 Dec 2022 13:15:04 +0100 Subject: [PATCH 04/29] Reset magnification while resetting zoom level (#882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1203515806054052/f Description: Follow Safari behavior of resetting pageZoom and magnification on ⌘0. --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/Browser Tab/View/WebView.swift | 16 ++-- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- Unit Tests/Browser Tab/WebViewTests.swift | 102 ++++++++++++++++++++++ 4 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 Unit Tests/Browser Tab/WebViewTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b755ab1a91..81656573fd 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -102,6 +102,7 @@ 378205F62837CBA800D1D4AA /* SavedStateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F52837CBA800D1D4AA /* SavedStateMock.swift */; }; 378205F8283BC6A600D1D4AA /* StartupPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */; }; 378205FB283C277800D1D4AA /* MainMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205FA283C277800D1D4AA /* MainMenuTests.swift */; }; + 3783F92329432E1800BCA897 /* WebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3783F92229432E1800BCA897 /* WebViewTests.swift */; }; 379DE4BD27EA31AC002CC3DE /* PreferencesAutofillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */; }; 37A4CEBA282E992F00D75B89 /* StartupPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */; }; 37A803DB27FD69D300052F4C /* Data Import Resources in Resources */ = {isa = PBXBuildFile; fileRef = 37A803DA27FD69D300052F4C /* Data Import Resources */; }; @@ -979,6 +980,7 @@ 378205F52837CBA800D1D4AA /* SavedStateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedStateMock.swift; sourceTree = ""; }; 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupPreferencesTests.swift; sourceTree = ""; }; 378205FA283C277800D1D4AA /* MainMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTests.swift; sourceTree = ""; }; + 3783F92229432E1800BCA897 /* WebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTests.swift; sourceTree = ""; }; 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesAutofillView.swift; sourceTree = ""; }; 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartupPreferences.swift; sourceTree = ""; }; 37A803DA27FD69D300052F4C /* Data Import Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Data Import Resources"; sourceTree = ""; }; @@ -3581,6 +3583,7 @@ children = ( B62EB47B25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift */, B67C6C3C2654B897006C872E /* WebViewExtensionTests.swift */, + 3783F92229432E1800BCA897 /* WebViewTests.swift */, B67C6C412654BF49006C872E /* DuckDuckGo-Symbol.jpg */, AA92ACAF24EFE209005F41C9 /* ViewModel */, AA92ACB024EFE210005F41C9 /* Model */, @@ -5493,6 +5496,7 @@ 4B117F7D276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift in Sources */, AAEC74B42642C69300C2EFBC /* HistoryCoordinatorTests.swift in Sources */, 378205F62837CBA800D1D4AA /* SavedStateMock.swift in Sources */, + 3783F92329432E1800BCA897 /* WebViewTests.swift in Sources */, EA8AE76A279FBDB20078943E /* ClickToLoadTDSTests.swift in Sources */, B63ED0DA26AE7AF400A9DAD1 /* PermissionManagerMock.swift in Sources */, AA9C362825518C44004B1BA3 /* WebsiteDataStoreMock.swift in Sources */, diff --git a/DuckDuckGo/Browser Tab/View/WebView.swift b/DuckDuckGo/Browser Tab/View/WebView.swift index e8ae85cec4..59568baa18 100644 --- a/DuckDuckGo/Browser Tab/View/WebView.swift +++ b/DuckDuckGo/Browser Tab/View/WebView.swift @@ -35,8 +35,8 @@ final class WebView: WKWebView { // MARK: - Zoom - static private let maxZoomLevel: CGFloat = 3.0 - static private let minZoomLevel: CGFloat = 0.5 + static let maxZoomLevel: CGFloat = 3.0 + static let minZoomLevel: CGFloat = 0.5 static private let zoomLevelStep: CGFloat = 0.1 var zoomLevel: CGFloat { @@ -47,16 +47,17 @@ final class WebView: WKWebView { return magnification } set { + let cappedValue = min(Self.maxZoomLevel, max(Self.minZoomLevel, newValue)) if #available(macOS 11.0, *) { - pageZoom = newValue + pageZoom = cappedValue } else { - magnification = newValue + magnification = cappedValue } } } var canZoomToActualSize: Bool { - self.window != nil && self.zoomLevel != 1.0 + self.window != nil && (self.zoomLevel != 1.0 || self.magnification != 1.0) } var canZoomIn: Bool { @@ -67,6 +68,11 @@ final class WebView: WKWebView { self.window != nil && self.zoomLevel > Self.minZoomLevel } + func resetZoomLevel() { + zoomLevel = 1.0 + magnification = 1.0 + } + func zoomIn() { guard canZoomIn else { return } self.zoomLevel = min(self.zoomLevel + Self.zoomLevelStep, Self.maxZoomLevel) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 8bde2243d2..0e62f19151 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -290,7 +290,7 @@ extension MainViewController { return } - selectedTabViewModel.tab.webView.zoomLevel = 1.0 + selectedTabViewModel.tab.webView.resetZoomLevel() } @IBAction func toggleDownloads(_ sender: Any) { diff --git a/Unit Tests/Browser Tab/WebViewTests.swift b/Unit Tests/Browser Tab/WebViewTests.swift new file mode 100644 index 0000000000..3bd1cc9529 --- /dev/null +++ b/Unit Tests/Browser Tab/WebViewTests.swift @@ -0,0 +1,102 @@ +// +// WebViewTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class WebViewTests: XCTestCase { + + typealias WebView = DuckDuckGo_Privacy_Browser.WebView + + let window = NSWindow() + var webView: WebView! + + override func setUp() { + webView = .init(frame: .zero) + window.contentView?.addSubview(webView) + } + + func testInitialZoomLevelAndMagnification() { + XCTAssertEqual(webView.zoomLevel, 1.0) + XCTAssertEqual(webView.magnification, 1.0) + } + + func testThatZoomInIncreasesZoomLevel() { + let zoomLevel = webView.zoomLevel + webView.zoomIn() + XCTAssertGreaterThan(webView.zoomLevel, zoomLevel) + } + + func testThatZoomOutDecreasesZoomLevel() { + let zoomLevel = webView.zoomLevel + webView.zoomOut() + XCTAssertLessThan(webView.zoomLevel, zoomLevel) + } + + func testThatZoomLevelIsCappedBetweenMinAndMaxValues() { + webView.zoomLevel = WebView.maxZoomLevel * 5 + XCTAssertEqual(webView.zoomLevel, WebView.maxZoomLevel) + + webView.zoomLevel = WebView.minZoomLevel * 0.1 + XCTAssertEqual(webView.zoomLevel, WebView.minZoomLevel) + } + + func testThatWebViewCannotBeZoomedInWhenAtMaxZoomLevel() { + XCTAssertTrue(webView.canZoomIn) + XCTAssertTrue(webView.canZoomOut) + webView.zoomLevel = WebView.maxZoomLevel + XCTAssertFalse(webView.canZoomIn) + XCTAssertTrue(webView.canZoomOut) + } + + func testThatWebViewCannotBeZoomedOutWhenAtMaxZoomLevel() { + XCTAssertTrue(webView.canZoomIn) + XCTAssertTrue(webView.canZoomOut) + webView.zoomLevel = WebView.minZoomLevel + XCTAssertTrue(webView.canZoomIn) + XCTAssertFalse(webView.canZoomOut) + } + + func testThatFreshWebViewInstanceCannotBeZoomedToActualSize() { + XCTAssertFalse(webView.canZoomToActualSize) + } + + func testWhenZoomLevelChangesThenWebViewCanBeZoomedToActualSize() { + webView.zoomLevel = 1.5 + XCTAssertTrue(webView.canZoomToActualSize) + } + + func testWhenMagnificationChangesThenWebViewCanBeZoomedToActualSize() { + webView.magnification = 1.5 + XCTAssertTrue(webView.canZoomToActualSize) + } + + func testWhenZoomLevelAndMagnificationChangeThenWebViewCanBeZoomedToActualSize() { + webView.zoomLevel = 0.7 + webView.magnification = 1.5 + XCTAssertTrue(webView.canZoomToActualSize) + } + + func testThatResetZoomLevelResetsZoomAndMagnification() { + webView.zoomLevel = 0.7 + webView.magnification = 1.5 + webView.resetZoomLevel() + XCTAssertEqual(webView.zoomLevel, 1.0) + XCTAssertEqual(webView.magnification, 1.0) + } +} From 06b338f116811ff1b2ec3a27703e062b389854cf Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Wed, 14 Dec 2022 23:07:52 +0100 Subject: [PATCH 05/29] Fix of SwiftUI crash (#895) Task/Issue URL: https://app.asana.com/0/1177771139624306/1203481412486290/f Description: PR addresses crashes caused by SwiftUI we receive from macOS Big Sur specifically. The idea of the fix is to avoid usage of LazyVStack. --- DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift | 4 ++-- DuckDuckGo/Home Page/View/FavoritesView.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift b/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift index f0f22d92b6..bd0b17ba75 100644 --- a/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift @@ -77,7 +77,7 @@ extension HomePage.Models { @Published private(set) var visibleModels: [FavoriteModel] = [] - @available(macOS, obsoleted: 11.0, message: "Use visibleModels and LazyVGrid instead") + @available(macOS, obsoleted: 12.0, message: "Use visibleModels and LazyVGrid instead") @Published private(set) var rows: [[FavoriteModel]] = [] let open: (Bookmark, OpenTarget) -> Void @@ -122,7 +122,7 @@ extension HomePage.Models { } private func updateVisibleModels() { - if #available(macOS 11.0, *) { + if #available(macOS 12.0, *) { visibleModels = showAllFavorites ? models : Array(models.prefix(HomePage.favoritesRowCountWhenCollapsed * HomePage.favoritesPerRow)) } else { rows = models.chunked(into: HomePage.favoritesPerRow) diff --git a/DuckDuckGo/Home Page/View/FavoritesView.swift b/DuckDuckGo/Home Page/View/FavoritesView.swift index 25afdf7c07..77dd30707b 100644 --- a/DuckDuckGo/Home Page/View/FavoritesView.swift +++ b/DuckDuckGo/Home Page/View/FavoritesView.swift @@ -32,7 +32,7 @@ struct Favorites: View { var body: some View { - if #available(macOS 11.0, *) { + if #available(macOS 12.0, *) { LazyVStack(spacing: 4) { FavoritesGrid(isHovering: $isHovering) } @@ -66,7 +66,7 @@ struct FavoritesGrid: View { var body: some View { - if #available(macOS 11.0, *) { + if #available(macOS 12.0, *) { LazyVGrid( columns: Array(repeating: GridItem(.fixed(GridDimensions.itemWidth), spacing: GridDimensions.horizontalSpacing), count: HomePage.favoritesPerRow), spacing: GridDimensions.verticalSpacing From b9a36528e510671e9dd52242c0913652a355ea5a Mon Sep 17 00:00:00 2001 From: Graeme Arthur <2030310+graeme@users.noreply.github.com> Date: Thu, 15 Dec 2022 12:44:46 +0100 Subject: [PATCH 06/29] Revert removal of History context lazy load (#902) Task/Issue URL: https://app.asana.com/0/1177771139624306/1203560842625353/f Description: There's been a rise in database.make.database.error Pixels 0.31.2. This could be due to a change in the timing of when the HistoryStore database gets initialised (it was previously lazy loaded, but it got moved into the initialiser). It's not crazy dramatic (about 5-10 per day) but we should fix it soon as the consequences are pretty bad (History not working). --- DuckDuckGo/History/Services/HistoryStore.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/History/Services/HistoryStore.swift b/DuckDuckGo/History/Services/HistoryStore.swift index 283f3271f4..3af51313c0 100644 --- a/DuckDuckGo/History/Services/HistoryStore.swift +++ b/DuckDuckGo/History/Services/HistoryStore.swift @@ -32,9 +32,9 @@ protocol HistoryStoring { final class HistoryStore: HistoryStoring { - private let context: NSManagedObjectContext - - init(context: NSManagedObjectContext = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History")) { + init() {} + + init(context: NSManagedObjectContext) { self.context = context } @@ -42,6 +42,8 @@ final class HistoryStore: HistoryStoring { case storeDeallocated case savingFailed } + + private lazy var context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History") func removeEntries(_ entries: [HistoryEntry]) -> Future { return Future { [weak self] promise in From 4ab6fb08d3368ff1a69212ad8027c59085bae1c2 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Thu, 15 Dec 2022 16:23:26 +0000 Subject: [PATCH 07/29] Fix imports from Bookmarks update to BSK (#867) Task/Issue URL: https://app.asana.com/0/72649045549333/1202533376181935/f Tech Design URL: CC: @bwaresiak @samsymons Description: Steps to test this PR: Smoke test app, especially around bookmarks database. Run the unit tests --- DuckDuckGo.xcodeproj/project.pbxproj | 10 +++++++++- DuckDuckGo/App Delegate/AppDelegate.swift | 1 + DuckDuckGo/Browser Tab/Model/Tab.swift | 1 + DuckDuckGo/Common/Database/Database.swift | 1 + Unit Tests/Content Blocker/ContentBlockingMock.swift | 2 +- Unit Tests/History/Services/HistoryStoreTests.swift | 2 +- 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 81656573fd..8949de1124 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -448,6 +448,7 @@ 9833913127AAA4B500DAF119 /* trackerData.json in Resources */ = {isa = PBXBuildFile; fileRef = 9833913027AAA4B500DAF119 /* trackerData.json */; }; 9833913327AAAEEE00DAF119 /* EmbeddedTrackerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9833913227AAAEEE00DAF119 /* EmbeddedTrackerDataTests.swift */; }; 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983DFB2428B67036006B7E34 /* UserContentUpdating.swift */; }; + 98A50964294B691800D10880 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 98A50963294B691800D10880 /* Persistence */; }; 98EB5D1027516A4800681FE6 /* AppPrivacyConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98EB5D0F27516A4800681FE6 /* AppPrivacyConfigurationTests.swift */; }; AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = AA06B6B62672AF8100F541C5 /* Sparkle */; }; AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */; }; @@ -1780,6 +1781,7 @@ 85FF55C825F82E4F00E2AB99 /* Lottie in Frameworks */, 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */, 1E25269C28F8741A00E44DFA /* Common in Frameworks */, + 98A50964294B691800D10880 /* Persistence in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4500,6 +4502,7 @@ 1E950E3E2912A10D0051A99B /* ContentBlocking */, 1E950E402912A10D0051A99B /* PrivacyDashboard */, 1E950E422912A10D0051A99B /* UserScript */, + 98A50963294B691800D10880 /* Persistence */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -6315,7 +6318,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 41.2.0; + version = 42.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -6377,6 +6380,11 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = BrowserServicesKit; }; + 98A50963294B691800D10880 /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; AA06B6B62672AF8100F541C5 /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */; diff --git a/DuckDuckGo/App Delegate/AppDelegate.swift b/DuckDuckGo/App Delegate/AppDelegate.swift index 927453cd3e..c109163447 100644 --- a/DuckDuckGo/App Delegate/AppDelegate.swift +++ b/DuckDuckGo/App Delegate/AppDelegate.swift @@ -20,6 +20,7 @@ import Cocoa import Combine import os.log import BrowserServicesKit +import Persistence @NSApplicationMain final class AppDelegate: NSObject, NSApplicationDelegate { diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index 81a57b0e51..af06d46220 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -26,6 +26,7 @@ import BrowserServicesKit import TrackerRadarKit import ContentBlocking import UserScript +import Common import PrivacyDashboard protocol TabDelegate: ContentOverlayUserScriptDelegate { diff --git a/DuckDuckGo/Common/Database/Database.swift b/DuckDuckGo/Common/Database/Database.swift index f7b89d511e..1027b1a5f6 100644 --- a/DuckDuckGo/Common/Database/Database.swift +++ b/DuckDuckGo/Common/Database/Database.swift @@ -19,6 +19,7 @@ import Foundation import CoreData import BrowserServicesKit +import Persistence final class Database { diff --git a/Unit Tests/Content Blocker/ContentBlockingMock.swift b/Unit Tests/Content Blocker/ContentBlockingMock.swift index fbc733192b..ea542606d2 100644 --- a/Unit Tests/Content Blocker/ContentBlockingMock.swift +++ b/Unit Tests/Content Blocker/ContentBlockingMock.swift @@ -47,7 +47,7 @@ final class ContentBlockingMock: NSObject, ContentBlockingProtocol, AdClickAttri var adClickAttributionRulesProvider: AdClickAttributionRulesProviding = MockAttributionRulesProvider() var attributionEvents: EventMapping? - var attributionDebugEvents: BrowserServicesKit.EventMapping? + var attributionDebugEvents: EventMapping? } diff --git a/Unit Tests/History/Services/HistoryStoreTests.swift b/Unit Tests/History/Services/HistoryStoreTests.swift index e88c476e8a..667d4a3810 100644 --- a/Unit Tests/History/Services/HistoryStoreTests.swift +++ b/Unit Tests/History/Services/HistoryStoreTests.swift @@ -19,7 +19,7 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser import Combine -import class BrowserServicesKit.CoreDataDatabase +import class Persistence.CoreDataDatabase final class HistoryStoreTests: XCTestCase { From 94fb9f2acbf1c0efa6feebe50c646736d38cc104 Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Wed, 21 Dec 2022 11:24:45 +0100 Subject: [PATCH 08/29] Unit test testWhenValuesAreAddedThenCallbacksAreCalled disabled (#901) Task/Issue URL: https://app.asana.com/0/0/1203560567694882/f Description: Few unit tests fail occasionally. Disabled until we have a reliable implementation --- .../xcschemes/DuckDuckGo Privacy Browser.xcscheme | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index 0e20ddd843..bf3b06f6e7 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -67,9 +67,15 @@ ReferencedContainer = "container:DuckDuckGo.xcodeproj"> + + + + From d7c3589f3dc5ef832a4693fd27677a0ef945eb3d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 21 Dec 2022 19:47:39 +0600 Subject: [PATCH 09/29] Fix opening new tab for user initated actions (#908) --- DuckDuckGo/Browser Tab/Model/Tab.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index af06d46220..82aee7460f 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -1167,7 +1167,7 @@ extension Tab: WKNavigationDelegate { let isLinkActivated = webView === sourceWebView && !isRedirect - && navigationAction.navigationType == .linkActivated + && (navigationAction.navigationType == .linkActivated || navigationAction.isUserInitiated) let isNavigatingAwayFromPinnedTab: Bool = { let isNavigatingToAnotherDomain = navigationAction.request.url?.host != url?.host From 8baab1d714b8c8b8047379685855d3ea13a0edc1 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 21 Dec 2022 20:49:37 +0600 Subject: [PATCH 10/29] fix Tabs drag&drop (#912) * fix Tabs drag&drop --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++ DuckDuckGo/Common/Utilities/Assertions.swift | 31 +++++++++++ DuckDuckGo/Tab Bar/Model/TabCollection.swift | 17 +++---- .../Tab Bar/View/TabBarViewController.swift | 51 ++++++++++--------- .../Tab Bar/View/TabDragAndDropManager.swift | 29 ++++------- .../ViewModel/TabCollectionViewModel.swift | 15 +----- .../Tab Bar/Model/TabCollectionTests.swift | 10 ++++ ...bCollectionViewModelTests+PinnedTabs.swift | 13 ----- ...wModelTests+WithoutPinnedTabsManager.swift | 33 ++++-------- .../TabCollectionViewModelTests.swift | 23 --------- 10 files changed, 103 insertions(+), 123 deletions(-) create mode 100644 DuckDuckGo/Common/Utilities/Assertions.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 8949de1124..722e957746 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -836,6 +836,7 @@ 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 */; }; + B6E319382953446000DD3BCF /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E319372953446000DD3BCF /* Assertions.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 */; }; @@ -1725,6 +1726,7 @@ 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 = ""; }; + B6E319372953446000DD3BCF /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = ""; }; B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExtension.swift; sourceTree = ""; }; B6E61EE7263ACE16004E11AB /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; }; @@ -2525,6 +2527,7 @@ isa = PBXGroup; children = ( 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */, + B6E319372953446000DD3BCF /* Assertions.swift */, 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */, 4BB88B5A25B7BA50006F6B06 /* Instruments.swift */, B6AAAC2C260330580029438D /* PublishedAfter.swift */, @@ -5277,6 +5280,7 @@ B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */, B6C00ECB292F839D009C73A6 /* AutofillTabExtension.swift in Sources */, 4B677439255DBEB800025BD8 /* AppHTTPSUpgradeStore.swift in Sources */, + B6E319382953446000DD3BCF /* Assertions.swift in Sources */, AAB549DF25DAB8F80058460B /* BookmarkViewModel.swift in Sources */, 85707F28276A34D900DC0649 /* DaxSpeech.swift in Sources */, 31F28C5328C8EECA00119F70 /* PrivatePlayerSchemeHandler.swift in Sources */, diff --git a/DuckDuckGo/Common/Utilities/Assertions.swift b/DuckDuckGo/Common/Utilities/Assertions.swift new file mode 100644 index 0000000000..ae3caf95d8 --- /dev/null +++ b/DuckDuckGo/Common/Utilities/Assertions.swift @@ -0,0 +1,31 @@ +// +// Assertions.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 + +#if DEBUG +public var customAssertionFailure: ((@autoclosure () -> String, StaticString, UInt) -> Void)? +public func assertionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { + customAssertionFailure?(message(), file, line) ?? Swift.assertionFailure(message(), file: file, line: line) +} + +public var customAssert: ((@autoclosure () -> Bool, @autoclosure () -> String, StaticString, UInt) -> Void)? +public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { + customAssert?(condition(), message(), file, line) ?? Swift.assert(condition(), message(), file: file, line: line) +} +#endif diff --git a/DuckDuckGo/Tab Bar/Model/TabCollection.swift b/DuckDuckGo/Tab Bar/Model/TabCollection.swift index f9b963f192..5c0645784b 100644 --- a/DuckDuckGo/Tab Bar/Model/TabCollection.swift +++ b/DuckDuckGo/Tab Bar/Model/TabCollection.swift @@ -18,7 +18,6 @@ import Foundation import Combine -import os.log final class TabCollection: NSObject { @@ -37,7 +36,7 @@ final class TabCollection: NSObject { @discardableResult func insert(_ tab: Tab, at index: Int) -> Bool { guard index >= 0, index <= tabs.endIndex else { - os_log("TabCollection: Index out of bounds", type: .error) + assertionFailure("TabCollection: Index out of bounds") return false } @@ -47,7 +46,7 @@ final class TabCollection: NSObject { func removeTab(at index: Int, published: Bool = true) -> Bool { guard tabs.indices.contains(index) else { - os_log("TabCollection: Index out of bounds", type: .error) + assertionFailure("TabCollection: Index out of bounds") return false } @@ -65,7 +64,7 @@ final class TabCollection: NSObject { guard let tab = tabs[safe: fromIndex], otherCollection.insert(tab, at: toIndex) else { - os_log("TabCollection: Index out of bounds", type: .error) + assertionFailure("TabCollection: Index out of bounds") return false } @@ -87,7 +86,7 @@ final class TabCollection: NSObject { guard !indexSet.contains(where: { index in index < 0 && index >= tabs.count }) else { - os_log("TabCollection: Index out of bounds", type: .error) + assertionFailure("TabCollection: Index out of bounds") return } @@ -113,8 +112,8 @@ final class TabCollection: NSObject { } func moveTab(at index: Int, to newIndex: Int) { - guard index >= 0, index < tabs.count, newIndex >= 0, newIndex < tabs.count else { - os_log("TabCollection: Index out of bounds", type: .error) + guard tabs.indices.contains(index), tabs.indices.contains(newIndex) else { + assertionFailure("TabCollection: Index out of bounds") return } @@ -130,8 +129,8 @@ final class TabCollection: NSObject { } func replaceTab(at index: Int, with tab: Tab) { - guard index >= 0, index < tabs.count else { - os_log("TabCollection: Index out of bounds", type: .error) + guard tabs.indices.contains(index) else { + assertionFailure("TabCollection: Index out of bounds") return } diff --git a/DuckDuckGo/Tab Bar/View/TabBarViewController.swift b/DuckDuckGo/Tab Bar/View/TabBarViewController.swift index f7848c42d5..9614866990 100644 --- a/DuckDuckGo/Tab Bar/View/TabBarViewController.swift +++ b/DuckDuckGo/Tab Bar/View/TabBarViewController.swift @@ -31,8 +31,8 @@ final class TabBarViewController: NSViewController { } @IBOutlet weak var pinnedTabsContainerView: NSView! - @IBOutlet weak var collectionView: TabBarCollectionView! - @IBOutlet weak var scrollView: TabBarScrollView! + @IBOutlet private weak var collectionView: TabBarCollectionView! + @IBOutlet private weak var scrollView: TabBarScrollView! @IBOutlet weak var pinnedTabsViewLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var pinnedTabsWindowDraggingView: WindowDraggingView! @IBOutlet weak var rightScrollButton: MouseOverButton! @@ -355,12 +355,14 @@ final class TabBarViewController: NSViewController { private var currentDraggingIndexPath: IndexPath? private func moveItemIfNeeded(at indexPath: IndexPath, to newIndexPath: IndexPath) { - guard newIndexPath != currentDraggingIndexPath else { return } + assert(indexPath == currentDraggingIndexPath) let index = indexPath.item - let newIndex = min(newIndexPath.item, max(tabCollectionViewModel.tabCollection.tabs.count - 1, 0)) - let newIndexPath = IndexPath(item: newIndex) - + let newIndex = newIndexPath.item + guard tabCollectionViewModel.tabCollection.tabs.indices.contains(newIndex) else { + assertionFailure("TabBarViewController: wrong index") + return + } guard index != newIndex else { return } currentDraggingIndexPath = newIndexPath @@ -371,7 +373,7 @@ final class TabBarViewController: NSViewController { private func moveToNewWindow(indexPath: IndexPath, droppingPoint: NSPoint? = nil) { guard tabCollectionViewModel.tabCollection.tabs.count > 1 else { return } guard let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item) else { - os_log("TabBarViewController: Failed to get tab view model", type: .error) + assertionFailure("TabBarViewController: Failed to get tab view model") return } @@ -806,6 +808,7 @@ extension TabBarViewController: NSCollectionViewDataSource { } func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + assert(collectionView.numberOfItems(inSection: 0) == tabCollectionViewModel.tabs.count) let item = collectionView.makeItem(withIdentifier: TabBarViewItem.identifier, for: indexPath) guard let tabBarViewItem = item as? TabBarViewItem else { assertionFailure("TabBarViewController: Failed to get reusable TabBarViewItem instance") @@ -853,7 +856,7 @@ extension TabBarViewController: NSCollectionViewDelegate { didChangeItemsAt indexPaths: Set, to highlightState: NSCollectionViewItem.HighlightState) { guard indexPaths.count == 1, let indexPath = indexPaths.first else { - os_log("TabBarViewController: More than 1 item highlighted", type: .error) + assertionFailure("TabBarViewController: More than 1 item highlighted") return } @@ -891,7 +894,7 @@ extension TabBarViewController: NSCollectionViewDelegate { initialDraggingIndexPaths = indexPaths guard let indexPath = indexPaths.first, indexPaths.count == 1 else { - os_log("TabBarViewController: More than 1 dragging index path", type: .error) + assertionFailure("TabBarViewController: More than 1 dragging index path") return } currentDraggingIndexPath = indexPath @@ -918,12 +921,12 @@ extension TabBarViewController: NSCollectionViewDelegate { // Create a new window if the drop is too distant let frameRelativeToWindow = view.convert(view.bounds, to: nil) guard let frameRelativeToScreen = view.window?.convertToScreen(frameRelativeToWindow) else { - os_log("TabBarViewController: Conversion to the screen coordinate system failed", type: .error) + assertionFailure("TabBarViewController: Conversion to the screen coordinate system failed") return } if !screenPoint.isNearRect(frameRelativeToScreen, allowedDistance: Self.dropToOpenDistance) { guard let draggingIndexPath = draggingIndexPath else { - os_log("TabBarViewController: No current dragging index path", type: .error) + assertionFailure("TabBarViewController: No current dragging index path") return } moveToNewWindow(indexPath: draggingIndexPath, droppingPoint: screenPoint) @@ -942,6 +945,8 @@ extension TabBarViewController: NSCollectionViewDelegate { return .private } + // clear inter-window drag destination when dragging returns back to origin + TabDragAndDropManager.shared.clearDestination() let newIndexPath = proposedDropIndexPath.pointee as IndexPath moveItemIfNeeded(at: currentDraggingIndexPath, to: newIndexPath) @@ -959,7 +964,7 @@ extension TabBarViewController: NSCollectionViewDelegate { } guard draggingIndexPaths.count == 1 else { - os_log("TabBarViewController: More than 1 item selected", type: .error) + assertionFailure("TabBarViewController: More than 1 item selected") return false } @@ -995,7 +1000,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemDuplicateAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return } @@ -1004,7 +1009,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return false } @@ -1013,7 +1018,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemPinAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return } @@ -1034,7 +1039,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return } @@ -1045,7 +1050,7 @@ extension TabBarViewController: TabBarViewItemDelegate { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let permissions = tabCollectionViewModel.tabViewModel(at: indexPath.item)?.tab.permissions else { - os_log("TabBarViewController: Failed to get index path of tab bar view item or its permissions", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item or its permissions") return } @@ -1060,7 +1065,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemCloseOtherAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return } @@ -1069,7 +1074,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemCloseToTheRightAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return } @@ -1078,7 +1083,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return } @@ -1089,7 +1094,7 @@ extension TabBarViewController: TabBarViewItemDelegate { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] else { - os_log("TabBarViewController: Failed to get tab from tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get tab from tab bar view item") return } @@ -1100,7 +1105,7 @@ extension TabBarViewController: TabBarViewItemDelegate { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] else { - os_log("TabBarViewController: Failed to get tab from tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get tab from tab bar view item") return } @@ -1109,7 +1114,7 @@ extension TabBarViewController: TabBarViewItemDelegate { func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") return .init(hasItemsToTheLeft: false, hasItemsToTheRight: false) } return .init(hasItemsToTheLeft: indexPath.item > 0, diff --git a/DuckDuckGo/Tab Bar/View/TabDragAndDropManager.swift b/DuckDuckGo/Tab Bar/View/TabDragAndDropManager.swift index 090cbfaeb9..95a99ef636 100644 --- a/DuckDuckGo/Tab Bar/View/TabDragAndDropManager.swift +++ b/DuckDuckGo/Tab Bar/View/TabDragAndDropManager.swift @@ -17,7 +17,6 @@ // import Foundation -import os.log /// Responsible for handling drag and drop of tabs between windows final class TabDragAndDropManager { @@ -33,19 +32,23 @@ final class TabDragAndDropManager { private var sourceUnit: Unit? private var destinationUnit: Unit? - private(set) var isDropRequested: Bool = false func setSource(tabCollectionViewModel: TabCollectionViewModel, indexPath: IndexPath) { sourceUnit = .init(tabCollectionViewModel: tabCollectionViewModel, indexPath: indexPath) } func setDestination(tabCollectionViewModel: TabCollectionViewModel, indexPath: IndexPath) { - isDropRequested = true + // ignore dragged objects from other apps + guard sourceUnit != nil else { return } destinationUnit = .init(tabCollectionViewModel: tabCollectionViewModel, indexPath: indexPath) } + func clearDestination() { + destinationUnit = nil + } + func performDragAndDropIfNeeded() -> Bool { - if isDropRequested { + if destinationUnit != nil { performDragAndDrop() clear() return true @@ -55,34 +58,22 @@ final class TabDragAndDropManager { } } - func dropToPinTabIfNeeded() -> Bool { - guard let sourceUnit = sourceUnit, - let sourceTabCollectionViewModel = sourceUnit.tabCollectionViewModel - else { - os_log("TabDragAndDropManager: Missing data to perform drop to pin", type: .error) - return false - } - sourceTabCollectionViewModel.pinTab(at: sourceUnit.indexPath.item) - return true - } - private func performDragAndDrop() { guard let sourceUnit = sourceUnit, let destinationUnit = destinationUnit, let sourceTabCollectionViewModel = sourceUnit.tabCollectionViewModel, let destinationTabCollectionViewModel = destinationUnit.tabCollectionViewModel else { - os_log("TabDragAndDropManager: Missing data to perform drag and drop", type: .error) + assertionFailure("TabDragAndDropManager: Missing data to perform drag and drop") return } - let newIndex = min(destinationUnit.indexPath.item + 1, destinationTabCollectionViewModel.tabCollection.tabs.count) + let newIndex = min(destinationUnit.indexPath.item + 1, destinationTabCollectionViewModel.tabCollection.tabs.count) sourceTabCollectionViewModel.moveTab(at: sourceUnit.indexPath.item, to: destinationTabCollectionViewModel, at: newIndex) } - func clear() { + private func clear() { sourceUnit = nil destinationUnit = nil - isDropRequested = false } } diff --git a/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift index b3a05b99db..3d2f8920ee 100644 --- a/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift @@ -41,7 +41,7 @@ final class TabCollectionViewModel: NSObject { weak var delegate: TabCollectionViewModelDelegate? /// Local tabs collection - private(set) var tabCollection: TabCollection + let tabCollection: TabCollection /// Pinned tabs collection (provided via `PinnedTabsManager` instance). var pinnedTabsCollection: TabCollection? { @@ -385,6 +385,7 @@ final class TabCollectionViewModel: NSObject { } func moveTab(at fromIndex: Int, to otherViewModel: TabCollectionViewModel, at toIndex: Int) { + assert(self !== otherViewModel) guard changesEnabled else { return } let parentTab = tabCollection.tabs[safe: fromIndex]?.parentTab @@ -457,18 +458,6 @@ final class TabCollectionViewModel: NSObject { delegate?.tabCollectionViewModelDidMultipleChanges(self) } - func remove(ownerOf webView: WebView) { - guard changesEnabled else { return } - - if let index = tabCollection.tabs.firstIndex(where: { $0.webView === webView }) { - remove(at: .unpinned(index)) - } else if let index = pinnedTabsCollection?.tabs.firstIndex(where: { $0.webView === webView }) { - remove(at: .pinned(index)) - } else { - os_log("TabCollection: Failed to get index of the tab", type: .error) - } - } - func removeSelected() { guard changesEnabled else { return } diff --git a/Unit Tests/Tab Bar/Model/TabCollectionTests.swift b/Unit Tests/Tab Bar/Model/TabCollectionTests.swift index 61902452a2..8ab53f21b0 100644 --- a/Unit Tests/Tab Bar/Model/TabCollectionTests.swift +++ b/Unit Tests/Tab Bar/Model/TabCollectionTests.swift @@ -21,6 +21,16 @@ import XCTest final class TabCollectionTests: XCTestCase { + override func setUp() { + customAssert = { _, _, _, _ in } + customAssertionFailure = { _, _, _ in } + } + + override func tearDown() { + customAssert = nil + customAssertionFailure = nil + } + // MARK: - Append func testWhenTabIsAppendedThenItsIndexIsLast() { diff --git a/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift b/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift index 618f17ab89..f98aa783b6 100644 --- a/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift +++ b/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+PinnedTabs.swift @@ -227,19 +227,6 @@ extension TabCollectionViewModelTests { XCTAssertEqual(tabCollectionViewModel.selectionIndex, .pinned(0)) } - func test_WithPinnedTabs_WhenPinnedOwnerOfWebviewIsRemovedThenAllOtherTabsRemained() { - let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModelWithPinnedTab() - - tabCollectionViewModel.appendNewTab() - tabCollectionViewModel.appendNewTab() - let pinnedTabViewModel = tabCollectionViewModel.pinnedTabsManager!.tabViewModel(at: 0)! - - tabCollectionViewModel.remove(ownerOf: pinnedTabViewModel.tab.webView) - - XCTAssertFalse(tabCollectionViewModel.pinnedTabsCollection!.tabs.contains(pinnedTabViewModel.tab)) - XCTAssertTrue(tabCollectionViewModel.pinnedTabsCollection!.tabs.isEmpty) - } - func test_WithPinnedTabs_RemoveSelected() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModelWithPinnedTab() tabCollectionViewModel.appendNewTab() diff --git a/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift b/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift index 688dd8acfa..8fde4313da 100644 --- a/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift +++ b/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests+WithoutPinnedTabsManager.swift @@ -23,6 +23,16 @@ import XCTest extension TabCollectionViewModelTests { + override func setUp() { + customAssert = { _, _, _, _ in } + customAssertionFailure = { _, _, _ in } + } + + override func tearDown() { + customAssert = nil + customAssertionFailure = nil + } + // MARK: - TabViewModel func test_WithoutPinnedTabsManager_WhenTabViewModelIsCalledWithIndexOutOfBoundsThenNilIsReturned() { @@ -368,29 +378,6 @@ extension TabCollectionViewModelTests { XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab, childTab1) } - func test_WithoutPinnedTabsManager_WhenOwnerOfWebviewIsRemovedThenAllOtherTabsRemained() { - let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() - - tabCollectionViewModel.appendNewTab() - tabCollectionViewModel.appendNewTab() - let lastTabViewModel = tabCollectionViewModel.tabViewModel(at: tabCollectionViewModel.tabCollection.tabs.count - 1)! - - tabCollectionViewModel.remove(ownerOf: lastTabViewModel.tab.webView) - - XCTAssertFalse(tabCollectionViewModel.tabCollection.tabs.contains(lastTabViewModel.tab)) - XCTAssert(tabCollectionViewModel.tabCollection.tabs.count == 2) - } - - func test_WithoutPinnedTabsManager_WhenOwnerOfWebviewIsNotInTabCollectionThenNoTabIsRemoved() { - let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() - let originalCount = tabCollectionViewModel.tabCollection.tabs.count - let tab = Tab() - - tabCollectionViewModel.remove(ownerOf: tab.webView) - - XCTAssertEqual(tabCollectionViewModel.tabCollection.tabs.count, originalCount) - } - func test_WithoutPinnedTabsManager_RemoveSelected() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() diff --git a/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests.swift b/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests.swift index 053c5c4d45..2153877f37 100644 --- a/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests.swift +++ b/Unit Tests/Tab Bar/ViewModel/TabCollectionViewModelTests.swift @@ -368,29 +368,6 @@ final class TabCollectionViewModelTests: XCTestCase { XCTAssertEqual(tabCollectionViewModel.selectedTabViewModel?.tab, childTab1) } - func testWhenOwnerOfWebviewIsRemovedThenAllOtherTabsRemained() { - let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() - - tabCollectionViewModel.appendNewTab() - tabCollectionViewModel.appendNewTab() - let lastTabViewModel = tabCollectionViewModel.tabViewModel(at: tabCollectionViewModel.tabCollection.tabs.count - 1)! - - tabCollectionViewModel.remove(ownerOf: lastTabViewModel.tab.webView) - - XCTAssertFalse(tabCollectionViewModel.tabCollection.tabs.contains(lastTabViewModel.tab)) - XCTAssert(tabCollectionViewModel.tabCollection.tabs.count == 2) - } - - func testWhenOwnerOfWebviewIsNotInTabCollectionThenNoTabIsRemoved() { - let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() - let originalCount = tabCollectionViewModel.tabCollection.tabs.count - let tab = Tab() - - tabCollectionViewModel.remove(ownerOf: tab.webView) - - XCTAssertEqual(tabCollectionViewModel.tabCollection.tabs.count, originalCount) - } - func testRemoveSelected() { let tabCollectionViewModel = TabCollectionViewModel.aTabCollectionViewModel() From 0ef81e0bcf689dc73af5eec2b291b813c861e704 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 21 Dec 2022 20:51:32 +0600 Subject: [PATCH 11/29] update linter rules and behaviour (#900) * update linter rules and behaviour * remove leading whitespaces --- .swiftlint.tests.yml | 51 ++++++ .swiftlint.yml | 11 +- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- DuckDuckGo/API/APIHeaders.swift | 2 +- DuckDuckGo/API/APIRequest.swift | 26 +-- DuckDuckGo/App Delegate/URLEventHandler.swift | 4 +- .../Autoconsent/AutoconsentUserScript.swift | 34 ++-- .../CookieConsentAnimationModel.swift | 18 +- .../CookieConsentAnimationView.swift | 2 +- .../Autoconsent/UI/CookieConsentPopover.swift | 32 ++-- .../UI/CookieConsentPopoverManager.swift | 18 +- .../UI/CookieConsentUserPermissionView.swift | 34 ++-- ...eConsentUserPermissionViewController.swift | 12 +- .../Autofill/ContentOverlayPopover.swift | 6 +- .../ContentOverlayViewController.swift | 19 +- .../View/BookmarksBarCollectionViewItem.swift | 80 ++++---- .../View/BookmarksBarViewController.swift | 80 ++++---- .../View/BookmarksBarViewModel.swift | 150 +++++++-------- .../View/HorizontallyCenteredLayout.swift | 12 +- DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 24 +-- .../Model/BookmarkManagedObject.swift | 2 +- .../Bookmarks/Model/BookmarkManager.swift | 16 +- .../Model/BookmarkOutlineViewDataSource.swift | 12 +- .../Bookmarks/Services/BookmarkStore.swift | 136 +++++++------- .../Bookmarks/Services/ContextualMenu.swift | 24 +-- .../View/AddBookmarkModalViewController.swift | 10 +- .../View/AddFolderModalViewController.swift | 4 +- .../View/BookmarkListViewController.swift | 118 ++++++------ ...okmarkManagementDetailViewController.swift | 40 ++-- ...kmarkManagementSidebarViewController.swift | 8 +- .../View/BookmarkPopoverViewController.swift | 32 ++-- .../View/BookmarkTableCellView.swift | 8 +- .../ViewModel/BookmarkViewModel.swift | 6 +- .../Extensions/HoveredLinkTabExtension.swift | 2 +- .../Model/ContentScopeFeatureFlagging.swift | 2 +- .../Browser Tab/Model/Tab+UIDelegate.swift | 2 +- DuckDuckGo/Browser Tab/Model/Tab.swift | 78 ++++---- .../Browser Tab/Model/UserDialogRequest.swift | 2 +- .../Services/WebsiteDataStore.swift | 8 +- .../View/WebViewContainerView.swift | 4 +- .../Browser Tab/ViewModel/TabViewModel.swift | 8 +- .../ViewModel/WebViewStateObserver.swift | 8 +- DuckDuckGo/Common/AppVersion.swift | 6 +- DuckDuckGo/Common/Database/Database.swift | 4 +- .../Extensions/ContiguousBytesExtension.swift | 2 +- .../Common/Extensions/DateExtension.swift | 2 +- .../Extensions/FileManagerExtension.swift | 2 +- .../Extensions/KeyedCodingExtension.swift | 6 +- .../Common/Extensions/LocaleExtension.swift | 6 +- .../Common/Extensions/NSColorExtension.swift | 14 +- .../Common/Extensions/NSImageExtensions.swift | 4 +- .../Extensions/NSMenuItemExtension.swift | 4 +- .../Extensions/NSPasteboardExtension.swift | 2 +- .../NSPasteboardItemExtension.swift | 10 +- .../Extensions/NSStackViewExtension.swift | 6 +- .../Extensions/NSTextFieldExtension.swift | 12 +- .../Extensions/NSTextViewExtension.swift | 4 +- .../NSViewControllerExtension.swift | 2 +- .../Common/Extensions/NSViewExtension.swift | 2 +- .../Common/Extensions/String+Punycode.swift | 2 +- .../Extensions/URLRequestExtension.swift | 2 +- .../WKUserContentControllerExtension.swift | 2 +- .../Extensions/WKWebViewExtension.swift | 8 +- .../WKWebsiteDataStoreExtension.swift | 14 +- .../EncryptionKeys/EncryptionKeyStore.swift | 2 +- DuckDuckGo/Common/File System/FileStore.swift | 4 +- .../File System/TemporaryFileHandler.swift | 28 +-- DuckDuckGo/Common/Localizables/UserText.swift | 50 ++--- DuckDuckGo/Common/Logging/Logging.swift | 16 +- .../Utilities/UserDefaultsWrapper.swift | 2 +- DuckDuckGo/Common/View/AppKit/ColorView.swift | 10 +- .../Common/View/AppKit/FlatButton.swift | 6 +- .../Common/View/AppKit/FocusRingView.swift | 2 +- .../Common/View/AppKit/GradientView.swift | 2 +- .../Common/View/AppKit/MouseOverButton.swift | 2 +- .../PersistentAppInterfaceSettings.swift | 6 +- .../View/AppKit/WindowDraggingView.swift | 2 +- .../SwiftUI/CustomRoundedCornersShape.swift | 20 +- .../Common/View/SwiftUI/HoverButton.swift | 2 +- .../View/SwiftUI/NSPathControlView.swift | 12 +- .../View/SwiftUI/NSPopUpButtonView.swift | 18 +- .../Common/View/SwiftUI/TextButton.swift | 6 +- .../Common/View/SwiftUI/ViewExtensions.swift | 2 +- .../ConfigurationDownloading.swift | 4 +- .../Configuration/ConfigurationStoring.swift | 6 +- .../Content Blocker/ClickToLoadModel.swift | 2 +- .../ClickToLoadUserScript.swift | 2 +- .../ContentBlockerRulesLists.swift | 16 +- .../Content Blocker/ContentBlocking.swift | 12 +- .../Crash Reports/Model/CrashReport.swift | 12 +- .../Model/CrashReportReader.swift | 2 +- .../Model/CrashReportSender.swift | 2 +- .../CrashReportPromptViewController.swift | 2 +- .../Bookmarks/BookmarkImport.swift | 2 +- .../Chromium/ChromiumFaviconsReader.swift | 12 +- .../Firefox/FirefoxBookmarksReader.swift | 4 +- .../Firefox/FirefoxFaviconsReader.swift | 12 +- .../Bookmarks/Safari/SafariDataImporter.swift | 8 +- .../Safari/SafariFaviconsReader.swift | 24 +-- .../Data Import/ChromePreferences.swift | 6 +- DuckDuckGo/Data Import/DataImport.swift | 22 +-- .../Logins/Chromium/BraveDataImporter.swift | 2 +- .../Logins/Chromium/ChromeDataImporter.swift | 2 +- .../Chromium/ChromiumDataImporter.swift | 12 +- .../Chromium/ChromiumKeychainPrompt.swift | 2 +- .../Logins/Chromium/ChromiumLoginReader.swift | 26 +-- .../Logins/Chromium/EdgeDataImporter.swift | 2 +- .../Logins/Firefox/ASN1Parser.swift | 4 +- .../Firefox/FirefoxBerkeleyDatabaseReader.m | 10 +- .../Logins/Firefox/FirefoxDataImporter.swift | 12 +- .../Firefox/FirefoxEncryptionKeyReader.swift | 66 +++---- .../Logins/Firefox/FirefoxLoginReader.swift | 16 +- .../Data Import/ThirdPartyBrowser.swift | 6 +- .../View/DataImportViewController.swift | 12 +- .../View/FileImportViewController.swift | 2 +- .../Data Import/View/NSAlert+DataImport.swift | 8 +- .../DeviceAuthenticationService.swift | 8 +- .../DeviceIdleStateDetector.swift | 4 +- .../LocalAuthenticationService.swift | 4 +- .../QuartzIdleStateProvider.swift | 6 +- .../Email/EmailManagerRequestDelegate.swift | 10 +- DuckDuckGo/Email/EmailUrlExtensions.swift | 2 +- .../Favicons/Model/FaviconManager.swift | 14 +- .../NSNotificationName+Favicons.swift | 2 +- .../View/FeedbackPresenter.swift | 2 +- .../View/FeedbackViewController.swift | 10 +- .../Model/DownloadListItem.swift | 2 +- .../Model/DownloadViewModel.swift | 2 +- .../Model/FileDownloadManager.swift | 4 +- .../Services/DownloadListCoordinator.swift | 2 +- .../Services/DownloadListStore.swift | 2 +- .../File Download/View/DownloadsPopover.swift | 2 +- .../View/DownloadsViewController.swift | 4 +- .../FindInPageViewController.swift | 6 +- DuckDuckGo/Fire/Model/Fire.swift | 56 +++--- DuckDuckGo/Fire/ViewModel/FireViewModel.swift | 2 +- .../History/Model/HistoryCoordinator.swift | 6 +- .../History/Services/HistoryStore.swift | 12 +- .../Model/HomePageFavoritesModel.swift | 4 +- .../Model/HomePageRecentlyVisitedModel.swift | 2 +- DuckDuckGo/Home Page/View/FavoritesView.swift | 24 +-- DuckDuckGo/Main/View/MainViewController.swift | 16 +- DuckDuckGo/Main/View/MainWindow.swift | 2 +- .../Main/View/MainWindowController.swift | 12 +- DuckDuckGo/Menus/MainMenu.swift | 4 +- DuckDuckGo/Menus/MainMenuActions.swift | 10 +- .../Navigation Bar/PinningManager.swift | 14 +- .../AddressBarButtonsViewController.swift | 36 ++-- .../View/AddressBarTextField.swift | 14 +- .../BadgeAnimationView.swift | 14 +- .../BadgeNotificationAnimationModel.swift | 2 +- ...okieManagedNotificationContainerView.swift | 20 +- .../CookieManagedNotificationView.swift | 36 ++-- .../CookieNotificationAnimationModel.swift | 6 +- .../NavigationBarBadgeAnimationView.swift | 12 +- .../NavigationBarBadgeAnimator.swift | 16 +- .../Navigation Bar/View/MoreOptionsMenu.swift | 56 +++--- .../View/NavigationBarViewController.swift | 64 +++---- DuckDuckGo/Onboarding/View/ActionSpeech.swift | 2 +- .../View/OnboardingViewController.swift | 2 +- .../ViewModel/OnboardingViewModel.swift | 2 +- .../Bitwarden/Model/BWManager.swift | 2 +- .../Bitwarden/Model/BWStatus.swift | 4 +- .../Services/BWInstallationService.swift | 4 +- .../Bitwarden/View/ConnectBitwardenView.swift | 120 ++++++------ .../View/ConnectBitwardenViewController.swift | 24 +-- .../View/ConnectBitwardenViewModel.swift | 38 ++-- .../PasswordManagerCoordinator.swift | 6 +- .../Permissions/Model/PermissionManager.swift | 3 +- .../Permissions/Model/PermissionState.swift | 2 +- .../Permissions/Model/Permissions.swift | 2 +- .../Pinned Tabs/View/PinnedTabView.swift | 14 +- .../Model/AutofillPreferences.swift | 2 +- .../Model/AutofillPreferencesModel.swift | 12 +- .../Model/DownloadsPreferences.swift | 14 +- DuckDuckGo/Preferences/View/Preferences.swift | 4 +- .../View/PreferencesAutofillView.swift | 28 +-- .../View/PreferencesViewController.swift | 2 +- .../ContentBlockingRulesUpdateObserver.swift | 22 +-- .../PrivacyDashboardPermissionHandler.swift | 38 ++-- .../View/PrivacyDashboardPopover.swift | 2 +- .../View/PrivacyDashboardViewController.swift | 58 +++--- .../WebsiteBreakageReporter.swift | 12 +- .../Extensions/UserText+PasswordManager.swift | 2 +- .../PasswordManagementIdentityModel.swift | 2 +- .../PasswordManagementItemListModel.swift | 38 ++-- .../Model/PasswordManagementListSection.swift | 28 +-- .../Model/SecureVaultSorting.swift | 22 +-- .../Secure Vault/Services/CountryList.swift | 2 +- .../Secure Vault/View/NSPathControlView.swift | 12 +- .../PasswordManagementBitwardenItemView.swift | 6 +- .../PasswordManagementIdentityItemView.swift | 40 ++-- .../View/PasswordManagementItemList.swift | 96 +++++----- .../View/PasswordManagementNoteItemView.swift | 20 +- .../View/PasswordManagementPopover.swift | 4 +- .../PasswordManagementViewController.swift | 14 +- .../Secure Vault/View/PopUpButton.swift | 32 ++-- .../View/SaveCredentialsViewController.swift | 16 +- .../View/SaveIdentityViewController.swift | 42 ++--- .../SavePaymentMethodViewController.swift | 26 +-- .../HTTPSBloomFilterSpecification.swift | 4 +- .../Smarter Encryption/HTTPSUpgrade.swift | 18 +- .../Store/AppHTTPSUpgradeStore.swift | 42 ++--- .../Statistics/ATB/AtbAndVariantCleanup.swift | 4 +- .../Statistics/ATB/LocalStatisticsStore.swift | 12 +- .../Statistics/ATB/StatisticsLoader.swift | 40 ++-- .../Statistics/ATB/StatisticsStore.swift | 6 +- DuckDuckGo/Statistics/PixelArguments.swift | 8 +- DuckDuckGo/Statistics/PixelDataStore.swift | 4 +- DuckDuckGo/Statistics/PixelEvent.swift | 64 +++---- DuckDuckGo/Statistics/PixelParameters.swift | 2 +- .../Statistics/RulesCompilationMonitor.swift | 2 +- .../Model/SuggestionContainer.swift | 2 +- .../View/SuggestionTableCellView.swift | 2 +- .../View/SuggestionViewController.swift | 4 +- .../SuggestionContainerViewModel.swift | 4 +- .../Tab Bar/View/TabBarCollectionView.swift | 8 +- .../Tab Bar/View/TabBarScrollView.swift | 2 +- .../Tab Bar/View/TabBarViewController.swift | 26 +-- DuckDuckGo/Tab Bar/View/TabBarViewItem.swift | 22 +-- DuckDuckGo/Tab Bar/View/TabShadowView.swift | 8 +- .../ViewModel/TabCollectionViewModel.swift | 4 +- .../View/TabPreviewWindowController.swift | 2 +- DuckDuckGo/User Agent/Model/UserAgent.swift | 2 +- .../View/WindowControllersManager.swift | 8 +- .../PrivatePlayerSchemeHandler.swift | 2 +- .../YoutubeOverlayUserScript.swift | 2 +- .../YoutubePlayerUserScript.swift | 14 +- UI Tests/TabBarTests.swift | 6 +- .../WindowManagerStateRestorationTests.swift | 1 - .../AutoconsentMessageProtocolTests.swift | 20 +- .../ConnectBitwardenViewModelTests.swift | 22 +-- .../BookmarksBarViewModelTests.swift | 32 ++-- .../Model/BookmarkManagedObjectTests.swift | 6 +- .../Services/BookmarkStoreMock.swift | 4 +- .../Services/LocalBookmarkStoreTests.swift | 171 +++++++++--------- Unit Tests/Browser Tab/Model/TabTests.swift | 2 +- .../Services/FaviconManagerMock.swift | 2 +- .../Services/WebsiteDataStoreTests.swift | 2 +- Unit Tests/Common/CoreDataTestUtilities.swift | 2 +- .../Common/Database/CoreDataStoreTests.swift | 2 +- .../Extensions/RunLoopExtensionTests.swift | 2 +- .../WKWebsiteDataStoreExtensionTests.swift | 14 +- .../Common/File System/FileStoreMock.swift | 8 +- .../TemporaryFileHandlerTests.swift | 10 +- Unit Tests/Common/FileSystemDSL.swift | 30 +-- Unit Tests/Common/FileSystemDSLTests.swift | 62 +++---- .../ConfigurationDownloaderTests.swift | 2 +- .../AppPrivacyConfigurationTests.swift | 10 +- .../Content Blocker/ClickToLoadTDSTests.swift | 14 +- .../ContentBlockingUpdatingTests.swift | 4 +- .../EmbeddedTrackerDataTests.swift | 30 +-- .../Crash Reports/CrashReportTests.swift | 16 +- Unit Tests/Data Export/MockSecureVault.swift | 4 +- .../Data Import/BrowserProfileTests.swift | 46 ++--- .../ChromiumLoginReaderTests.swift | 20 +- .../FirefoxBookmarksReaderTests.swift | 2 +- .../FirefoxDataImporterTests.swift | 18 +- .../Data Import/FirefoxKeyReaderTests.swift | 36 ++-- .../Data Import/FirefoxLoginReaderTests.swift | 92 +++++----- .../Data Import/ThirdPartyBrowserTests.swift | 20 +- .../DownloadListCoordinatorTests.swift | 4 +- .../FileDownloadManagerMock.swift | 2 +- .../Geolocation/CLLocationManagerMock.swift | 2 +- .../GeolocationProviderTests.swift | 10 +- .../Geolocation/GeolocationServiceTests.swift | 4 - .../Model/HistoryCoordinatorTests.swift | 26 +-- .../History/Services/HistoryStoreTests.swift | 54 +++--- .../RecentlyVisitedSiteModelTests.swift | 7 +- Unit Tests/Menus/MainMenuTests.swift | 2 - .../LocalPinningManagerTests.swift | 28 +-- Unit Tests/Onboarding/OnboardingTests.swift | 3 +- .../Permissions/PermissionModelTests.swift | 1 - .../Permissions/PermissionStoreTests.swift | 2 +- Unit Tests/Permissions/WebViewMock.swift | 14 +- .../AutofillPreferencesModelTests.swift | 1 - .../DefaultBrowserPreferencesTests.swift | 3 - .../DownloadsPreferencesTests.swift | 11 +- .../PreferencesSidebarModelTests.swift | 1 - .../BrokenSiteReportingReferenceTests.swift | 26 +-- .../PrivacyReferenceTestHelper.swift | 14 +- ...PasswordManagementItemListModelTests.swift | 20 +- .../PasswordManagementListSectionTests.swift | 34 ++-- .../ATB/AtbAndVariantCleanupTests.swift | 2 +- .../ATB/Mock/MockVariantManager.swift | 4 +- .../ATB/StatisticsLoaderTests.swift | 16 +- .../Statistics/ATB/VariantManagerTests.swift | 16 +- .../CBRCompileTimeReporterTests.swift | 2 +- .../LocalStatisticsStoreTests.swift | 34 ++-- Unit Tests/Statistics/PixelEventTests.swift | 2 +- Unit Tests/Statistics/PixelTests.swift | 4 +- .../SuggestionContainerViewModelTests.swift | 8 +- .../ViewModel/SuggestionViewModelTests.swift | 4 +- .../ViewModel/TabLazyLoaderTests.swift | 1 - .../User Agent/Model/UserAgentTests.swift | 4 +- 295 files changed, 2278 insertions(+), 2252 deletions(-) create mode 100644 .swiftlint.tests.yml diff --git a/.swiftlint.tests.yml b/.swiftlint.tests.yml new file mode 100644 index 0000000000..208a1e9d28 --- /dev/null +++ b/.swiftlint.tests.yml @@ -0,0 +1,51 @@ +# swiftlint config applied for Tests + +disabled_rules: + - no_space_in_method_call + - multiple_closures_with_trailing_closure + - block_based_kvo + - compiler_protocol_init + - unused_setter_value + - line_length + - type_name + - force_cast + - force_try + - function_body_length + - cyclomatic_complexity + - identifier_name + - implicit_getter + +opt_in_rules: + - file_header + +# Rule Config +file_length: + warning: 1800 + error: 2000 +type_body_length: + warning: 1000 + error: 1500 +nesting: + type_level: 4 +large_tuple: + warning: 6 + error: 10 +file_header: + required_pattern: | + \/\/ + \/\/ .*?\.swift + \/\/ + \/\/ Copyright © \d{4} 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\. + \/\/ diff --git a/.swiftlint.yml b/.swiftlint.yml index b950029df3..5401d918ff 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,4 @@ disabled_rules: - - trailing_whitespace - no_space_in_method_call - multiple_closures_with_trailing_closure - block_based_kvo @@ -7,6 +6,8 @@ disabled_rules: - unused_setter_value - line_length - type_name + - implicit_getter + - function_parameter_count opt_in_rules: - file_header @@ -14,7 +15,6 @@ opt_in_rules: custom_rules: explicit_non_final_class: included: ".*\\.swift" - excluded: ".*Tests\\.swift" name: "Implicitly non-final class" regex: "^\\s*(class) (?!func|var)" capture_group: 0 @@ -35,6 +35,9 @@ type_body_length: error: 500 nesting: type_level: 2 +large_tuple: + warning: 4 + error: 5 file_header: required_pattern: | \/\/ @@ -58,4 +61,6 @@ file_header: # General Config excluded: - DuckDuckGo/Common/Localizables/UserText.swift - + - Unit Tests + - Integration Tests + - UI Tests diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 722e957746..6be5245d30 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -4739,7 +4739,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/zsh; - shellScript = "if [[ -n \"$CI\" ]]; then\n echo \"Skipping SwiftLint run in CI\"\n exit 0\nfi\n\n# Add brew into PATH\nif [[ -f /opt/homebrew/bin/brew ]]; then\n eval $(/opt/homebrew/bin/brew shellenv)\nfi\n\nif which swiftlint >/dev/null; then\n if [ \"$CONFIGURATION\" = \"Release\" ]; then\n swiftlint lint --strict\n if [ $? -ne 0 ]; then\n echo \"error: SwiftLint validation failed.\"\n exit 1\n fi\n else\n swiftlint lint\n fi\nelse\n echo \"error: SwiftLint not installed. Install using \\`brew install swiftlint\\`\"\n exit 1\nfi\n"; + shellScript = "if [[ -n \"$CI\" ]]; then\n echo \"Skipping SwiftLint run in CI\"\n exit 0\nfi\n\n# Add brew into PATH\nif [[ -f /opt/homebrew/bin/brew ]]; then\n eval $(/opt/homebrew/bin/brew shellenv)\nfi\n\nrun_swiftlint_for_modified_files () {\n TEST_FILES=\"\"\n CODE_FILES=\"\"\n\n # collect staged and unstaged files and replace spaces in filenames with #001\n for FILE_NAME in $({ git diff --name-only & git diff --cached --name-only; } | tr ' ' '\\001' | tr '\\n ' ' ')\n do\n echo \"handling $FILE_NAME\"\n # collect .swift files separately for Unit Tests and Code Files\n if [[ \"${FILE_NAME##*.}\" == \"swift\" ]]; then\n if [[ \"$FILE_NAME\" == *\"Tests/\"* ]]; then\n TEST_FILES+=\" \\\"${FILE_NAME}\\\"\"\n else\n CODE_FILES+=\" \\\"${FILE_NAME}\\\"\"\n fi\n fi\n done\n\n if [ -n \"${CODE_FILES}\" ]; then\n # replace back #001 with space and feed to swiftlint\n echo \"${CODE_FILES}\" | tr '\\001' ' ' | xargs swiftlint lint\n fi\n if [ -n \"${TEST_FILES}\" ]; then\n echo \"${TEST_FILES}\" | tr '\\001' ' ' | xargs swiftlint lint --config .swiftlint.tests.yml\n fi\n}\n\nif which swiftlint >/dev/null; then\n if [ \"$CONFIGURATION\" = \"Release\" ]; then\n swiftlint lint --strict\n if [ $? -ne 0 ]; then\n echo \"error: SwiftLint validation failed.\"\n exit 1\n fi\n else\n run_swiftlint_for_modified_files\n fi\nelse\n echo \"error: SwiftLint not installed. Install using \\`brew install swiftlint\\`\"\n exit 1\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/DuckDuckGo/API/APIHeaders.swift b/DuckDuckGo/API/APIHeaders.swift index 7de733df1c..274066a79b 100644 --- a/DuckDuckGo/API/APIHeaders.swift +++ b/DuckDuckGo/API/APIHeaders.swift @@ -44,7 +44,7 @@ final class APIHeaders { let q = 1.0 - (Double(index) * 0.1) return "\(language);q=\(q)" }.joined(separator: ", ") - + return [ Name.acceptEncoding: acceptEncoding, Name.acceptLanguage: acceptLanguage, diff --git a/DuckDuckGo/API/APIRequest.swift b/DuckDuckGo/API/APIRequest.swift index c4a48b766d..2698db5457 100644 --- a/DuckDuckGo/API/APIRequest.swift +++ b/DuckDuckGo/API/APIRequest.swift @@ -22,7 +22,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" @@ -33,18 +33,18 @@ enum APIRequest { private static let defaultCallbackSession = URLSession(configuration: .default, delegate: nil, delegateQueue: defaultCallbackQueue) private static let defaultCallbackEphemeralSession = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: defaultCallbackQueue) - + private static let mainThreadCallbackSession = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main) private static let mainThreadCallbackEphemeralSession = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: OperationQueue.main) struct Response { - + var data: Data? var etag: String? var urlResponse: URLResponse? - + } - + enum HTTPMethod: String { case get = "GET" case head = "HEAD" @@ -67,7 +67,7 @@ enum APIRequest { useEphemeralURLSession: Bool = true, // URL requests must opt into using shared storage callBackOnMainThread: Bool = false, completion: @escaping APIRequestCompletion) -> URLSessionDataTask { - + let urlRequest = urlRequestFor( url: url, method: method, @@ -84,11 +84,11 @@ enum APIRequest { if let error = error { completion(nil, error) - } else if let error = httpResponse?.validateStatusCode(statusCode: 200..<300) { + } else if let error = httpResponse?.validateStatusCode(statusCode: 200..<300) { completion(nil, error) } else { var etag = httpResponse?.headerValue(for: APIHeaders.Name.etag) - + // Handle weak etags etag = etag?.dropping(prefix: "W/") completion(Response(data: data, etag: etag, urlResponse: response), nil) @@ -97,7 +97,7 @@ enum APIRequest { task.resume() return task } - + static func urlRequestFor(url: URL, method: HTTPMethod = .get, parameters: [String: String]? = nil, @@ -110,7 +110,7 @@ enum APIRequest { urlRequest.httpMethod = method.rawValue return urlRequest } - + private static func session(useMainThreadCallbackQueue: Bool, ephemeral: Bool) -> URLSession { if useMainThreadCallbackQueue { return ephemeral ? mainThreadCallbackEphemeralSession : mainThreadCallbackSession @@ -122,15 +122,15 @@ enum APIRequest { } extension HTTPURLResponse { - + enum HTTPURLResponseError: Error { case invalidStatusCode } - + func validateStatusCode(statusCode acceptedStatusCodes: S) -> Error? where S.Iterator.Element == Int { return acceptedStatusCodes.contains(statusCode) ? nil : HTTPURLResponseError.invalidStatusCode } - + fileprivate func headerValue(for name: String) -> String? { let lname = name.lowercased() return allHeaderFields.filter { ($0.key as? String)?.lowercased() == lname }.first?.value as? String diff --git a/DuckDuckGo/App Delegate/URLEventHandler.swift b/DuckDuckGo/App Delegate/URLEventHandler.swift index ab9fd5b523..f748cb33ab 100644 --- a/DuckDuckGo/App Delegate/URLEventHandler.swift +++ b/DuckDuckGo/App Delegate/URLEventHandler.swift @@ -25,7 +25,7 @@ final class URLEventHandler { private var didFinishLaunching = false private var urlsToOpen = [URL]() - + init(handler: @escaping ((URL) -> Void) = openURL) { self.handler = handler @@ -46,7 +46,7 @@ final class URLEventHandler { self.urlsToOpen = [] } - + didFinishLaunching = true } diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index 6f1c7d67c9..da32745038 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -107,13 +107,13 @@ extension AutoconsentUserScript { let id: String let code: String } - + struct PopupFoundMessage: Codable { let type: String let cmp: String let url: String } - + struct OptOutResultMessage: Codable { let type: String let cmp: String @@ -121,7 +121,7 @@ extension AutoconsentUserScript { let scheduleSelfTest: Bool let url: String } - + struct OptInResultMessage: Codable { let type: String let cmp: String @@ -129,20 +129,20 @@ extension AutoconsentUserScript { let scheduleSelfTest: Bool let url: String } - + struct SelfTestResultMessage: Codable { let type: String let cmp: String let result: Bool let url: String } - + struct AutoconsentDoneMessage: Codable { let type: String let cmp: String let url: String } - + func decodeMessageBody(from message: Input) -> Target? { do { let json = try JSONSerialization.data(withJSONObject: message) @@ -249,7 +249,7 @@ extension AutoconsentUserScript { ] ], nil) } - + @MainActor func handleEval(message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) { guard let messageData: EvalMessage = decodeMessageBody(from: message.body) else { @@ -266,7 +266,7 @@ extension AutoconsentUserScript { } })(); """ - + if let webview = message.webView { webview.evaluateJavaScript(script, in: message.frameInfo, in: WKContentWorld.page, completionHandler: { (result) in switch result { @@ -287,7 +287,7 @@ extension AutoconsentUserScript { replyHandler(nil, "missing frame target") } } - + @MainActor func handlePopupFound(message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) { guard preferences.autoconsentEnabled == nil else { @@ -295,7 +295,7 @@ extension AutoconsentUserScript { replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection return } - + os_log("Prompting user about autoconsent", log: .autoconsent, type: .debug) // if it's the first time, prompt the user and trigger opt-out @@ -311,7 +311,7 @@ extension AutoconsentUserScript { replyHandler(nil, "missing frame target") } } - + @MainActor func handleOptOutResult(message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) { guard let messageData: OptOutResultMessage = decodeMessageBody(from: message.body) else { @@ -330,7 +330,7 @@ extension AutoconsentUserScript { replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection } - + @MainActor func handleAutoconsentDone(message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) { // report a managed popup @@ -339,15 +339,15 @@ extension AutoconsentUserScript { return } os_log("opt-out successful: %s", log: .autoconsent, type: .debug, String(describing: messageData)) - + guard let url = URL(string: messageData.url), let host = url.host else { replyHandler(nil, "cannot decode message") return } - + refreshDashboardState(consentManaged: true, optoutFailed: false, selftestFailed: nil) - + // trigger popup once per domain if !management.sitesNotifiedCache.contains(host) { os_log("bragging that we closed a popup", log: .autoconsent, type: .debug) @@ -359,7 +359,7 @@ extension AutoconsentUserScript { ]) } } - + replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection if let selfTestWebView = selfTestWebView, @@ -384,7 +384,7 @@ extension AutoconsentUserScript { selfTestWebView = nil selfTestFrameInfo = nil } - + @MainActor func handleSelfTestResult(message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) { guard let messageData: SelfTestResultMessage = decodeMessageBody(from: message.body) else { diff --git a/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationModel.swift b/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationModel.swift index 8bbff8bae7..2872bc5d25 100644 --- a/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationModel.swift +++ b/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationModel.swift @@ -33,18 +33,18 @@ protocol CookieConsentAnimation: ObservableObject { } final class CookieConsentAnimationModel: CookieConsentAnimation { - + private enum Animation { struct AnimationValue { let begin: CGFloat let end: CGFloat } - + enum Image { static let opacity = AnimationValue(begin: 0, end: 1) static let scale = AnimationValue(begin: 0, end: 1) } - + enum Pills { static let opacity = AnimationValue(begin: 0, end: 1) static let scale = AnimationValue(begin: 0, end: 1) @@ -52,32 +52,32 @@ final class CookieConsentAnimationModel: CookieConsentAnimation { static let rightSideOffset = AnimationValue(begin: -30, end: 0) } } - + let firstAnimationDuration: CGFloat = 0.35 let secondAnimationDuration: CGFloat = 0.3 - + @Published var imageOpacity = Animation.Image.opacity.begin @Published var imageScale = Animation.Image.scale.begin @Published var pillsOpacity = Animation.Pills.opacity.begin @Published var pillsScale = Animation.Pills.scale.begin @Published var pillLeftSideOffset = Animation.Pills.leftSideOffset.begin @Published var pillRightSideOffset = Animation.Pills.rightSideOffset.begin - + private func updateDataForFirstAnimation() { imageOpacity = Animation.Image.opacity.end imageScale = Animation.Image.scale.end } - + private func updateDataForSecondAnimation() { pillsOpacity = Animation.Pills.opacity.end pillsScale = Animation.Pills.scale.end pillRightSideOffset = Animation.Pills.rightSideOffset.end pillLeftSideOffset = Animation.Pills.leftSideOffset.end } - + func startAnimation() { updateDataForFirstAnimation() - + DispatchQueue.main.asyncAfter(deadline: .now() + (firstAnimationDuration) / 2) { self.updateDataForSecondAnimation() } diff --git a/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationView.swift b/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationView.swift index c79d54e278..79ed99cf11 100644 --- a/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationView.swift +++ b/DuckDuckGo/Autoconsent/UI/Animation/CookieConsentAnimationView.swift @@ -20,7 +20,7 @@ import SwiftUI struct CookieConsentAnimationView: View where AnimationModel: CookieConsentAnimation { @ObservedObject var animationModel: AnimationModel - + var body: some View { VStack { HStack { diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift index 272bc41115..46fa5558ff 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift @@ -37,33 +37,33 @@ public final class CookieConsentPopover { let storyboard = NSStoryboard(name: "CookieConsent", bundle: Bundle.main) viewController = storyboard.instantiateController(identifier: "CookieConsentUserPermissionViewController") windowController = storyboard.instantiateController(identifier: "CookieConsentWindowController") - + windowController.contentViewController = viewController windowController.window?.acceptsMouseMovedEvents = true windowController.window?.ignoresMouseEvents = false - + viewController.view.window?.backgroundColor = .clear viewController.view.wantsLayer = true - + viewController.delegate = self } - + public func close(animated: Bool, completion: (() -> Void)? = nil) { guard let overlayWindow = windowController.window else { return } if !overlayWindow.isVisible { return } - + let removeWindow = { overlayWindow.parent?.removeChildWindow(overlayWindow) overlayWindow.orderOut(nil) completion?() } - + if animated { NSAnimationContext.runAnimationGroup { context in context.duration = AnimationConsts.duration - + let newOrigin = NSPoint(x: overlayWindow.frame.origin.x, y: overlayWindow.frame.origin.y + AnimationConsts.yAnimationOffset) let size = overlayWindow.frame.size overlayWindow.animator().alphaValue = 0 @@ -75,12 +75,12 @@ public final class CookieConsentPopover { removeWindow() } } - + private func windowDidResize(_ parent: NSWindow) { guard let overlayWindow = windowController.window else { return } - + let xPosition = (parent.frame.width / 2) - (overlayWindow.frame.width / 2) + parent.frame.origin.x let yPosition = parent.frame.origin.y + parent.frame.height - overlayWindow.frame.height - AnimationConsts.yAnimationOffset @@ -88,7 +88,7 @@ public final class CookieConsentPopover { let newOrigin = NSPoint(x: xPosition, y: yPosition) overlayWindow.setFrame(NSRect(origin: newOrigin, size: size), display: true) } - + private func addObserverForWindowResize(_ window: NSWindow) { resizeObserver = NotificationCenter.default.addObserver(forName: NSWindow.didResizeNotification, object: window, @@ -97,7 +97,7 @@ public final class CookieConsentPopover { self?.windowDidResize(parent) } } - + public func show(on currentTabView: NSView, animated: Bool) { guard let currentTabViewWindow = currentTabView.window, let overlayWindow = windowController.window else { @@ -105,16 +105,16 @@ public final class CookieConsentPopover { } addObserverForWindowResize(currentTabViewWindow) - + currentTabViewWindow.addChildWindow(overlayWindow, ordered: .above) - + let xPosition = (currentTabViewWindow.frame.width / 2) - (overlayWindow.frame.width / 2) + currentTabViewWindow.frame.origin.x let yPosition = currentTabViewWindow.frame.origin.y + currentTabViewWindow.frame.height - overlayWindow.frame.height - + if animated { overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) overlayWindow.alphaValue = 0 - + NSAnimationContext.runAnimationGroup { context in context.duration = AnimationConsts.duration let newOrigin = NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset) @@ -130,7 +130,7 @@ public final class CookieConsentPopover { overlayWindow.setFrameOrigin(NSPoint(x: xPosition, y: yPosition - AnimationConsts.yAnimationOffset)) } } - + public required init?(coder: NSCoder) { fatalError("CookieConsentPopover: Bad initializer") } diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift index 299139e64f..44e05b560d 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift @@ -21,23 +21,23 @@ import Foundation final class CookieConsentPopoverManager: CookieConsentPopoverDelegate { var completion: ((Bool) -> Void)? weak var currentTab: Tab? - + private(set) var popOver: CookieConsentPopover? - + func cookieConsentPopover(_ popOver: CookieConsentPopover, didFinishWithResult result: Bool) { popOver.close(animated: true) { [weak self] in self?.popOver = nil self?.currentTab = nil } - + if let completion = completion { completion(result) } } - + func show(on view: NSView, animated: Bool, result: ((Bool) -> Void)? = nil) { preparePopover() - + guard let popOver = popOver else { return } @@ -47,12 +47,12 @@ final class CookieConsentPopoverManager: CookieConsentPopoverDelegate { self.completion = result } } - + func close(animated: Bool) { guard let popOver = popOver else { return } - + popOver.close(animated: animated) } @@ -61,11 +61,11 @@ final class CookieConsentPopoverManager: CookieConsentPopoverDelegate { if currentTab == nil { popOver = nil } - + guard popOver == nil else { return } - + popOver = CookieConsentPopover() popOver?.delegate = self } diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionView.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionView.swift index df1586addf..cacd102cb2 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionView.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionView.swift @@ -44,7 +44,7 @@ struct CookieConsentUserPermissionView: View where AnimationMode .stroke(colorScheme == .dark ? Consts.Colors.darkModeBorderColor : Consts.Colors.whiteModeBorderColor, lineWidth: 1) ) } - + private var daxStackView: some View { VStack { HStack { @@ -52,28 +52,28 @@ struct CookieConsentUserPermissionView: View where AnimationMode .resizable() .frame(width: Consts.Layout.daxImageSize, height: Consts.Layout.daxImageSize) .shadow(color: Consts.Colors.daxShadow, radius: 6, x: 0, y: 3) - + Spacer() } } } - + private var contentView: some View { VStack(alignment: .leading, spacing: 24) { Text(UserText.autoconsentModalTitle) .font(.system(size: Consts.Font.size)) .fontWeight(.light) - + CookieConsentAnimationView(animationModel: sketchAnimationModel) .padding(.leading, 40) - + Text(UserText.autoconsentModalBody) .fontWeight(.light) .font(.system(size: Consts.Font.size)) - + }.frame(maxHeight: .infinity) } - + private var buttonStack: some View { HStack { Button { @@ -82,7 +82,7 @@ struct CookieConsentUserPermissionView: View where AnimationMode Text(UserText.autoconsentModalDenyButton) } .buttonStyle(SecondaryCTAStyle()) - + Button { result(true) } label: { @@ -91,7 +91,7 @@ struct CookieConsentUserPermissionView: View where AnimationMode .buttonStyle(PrimaryCTAStyle()) } } - + func startAnimation() { sketchAnimationModel.startAnimation() } @@ -100,7 +100,7 @@ struct CookieConsentUserPermissionView: View where AnimationMode struct CookieConsentUserPermissionView_Previews: PreviewProvider { static var previews: some View { let result: (Bool) -> Void = { _ in } - + if #available(macOS 11.0, *) { CookieConsentUserPermissionView(sketchAnimationModel: CookieConsentAnimationMock(), result: result).preferredColorScheme(.dark) .padding() @@ -113,7 +113,7 @@ struct CookieConsentUserPermissionView_Previews: PreviewProvider { } private struct PrimaryCTAStyle: ButtonStyle { - + func makeBody(configuration: Self.Configuration) -> some View { let color = configuration.isPressed ? Color("CookieConsentPrimaryButtonPressed") : Color("CookieConsentPrimaryButton") @@ -130,13 +130,13 @@ private struct PrimaryCTAStyle: ButtonStyle { private struct SecondaryCTAStyle: ButtonStyle { @Environment(\.colorScheme) var colorScheme - + func makeBody(configuration: Self.Configuration) -> some View { - + let color = configuration.isPressed ? Color("CookieConsentSecondaryButtonPressed") : Color("CookieConsentSecondaryButton") - + let outterShadowOpacity = colorScheme == .dark ? 0.8 : 0.0 - + configuration.label .font(.system(size: 13, weight: .light, design: .default)) .foregroundColor(.primary) @@ -147,7 +147,7 @@ private struct SecondaryCTAStyle: ButtonStyle { .fill(color) .shadow(color: .black.opacity(0.1), radius: 0.1, x: 0, y: 1) .shadow(color: .primary.opacity(outterShadowOpacity), radius: 0.1, x: 0, y: -0.6)) - + .overlay( RoundedRectangle(cornerRadius: Consts.Layout.CTACornerRadius) .stroke(Color.black.opacity(0.1), lineWidth: 1)) @@ -164,7 +164,7 @@ private enum Consts { static let CTACornerRadius: CGFloat = 8 static let containerPadding: CGFloat = 20 } - + struct Colors { static let darkModeBorderColor: Color = .white.opacity(0.2) static let whiteModeBorderColor: Color = .black.opacity(0.1) diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionViewController.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionViewController.swift index 54b930692d..2b60f6f717 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionViewController.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentUserPermissionViewController.swift @@ -35,26 +35,26 @@ public final class CookieConsentUserPermissionViewController: NSViewController { } return NSHostingView(rootView: permissionView) }() - + public override func loadView() { view = NSView(frame: NSRect(origin: CGPoint.zero, size: viewSize)) } - + public override func viewDidLoad() { super.viewDidLoad() - + view.addSubview(consentView) setupConstraints() view.applyDropShadow() } - + public func startAnimation() { sketchAnimationModel.startAnimation() } - + private func setupConstraints() { consentView.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ consentView.heightAnchor.constraint(equalToConstant: viewSize.height), consentView.widthAnchor.constraint(equalToConstant: viewSize.width), diff --git a/DuckDuckGo/Autofill/ContentOverlayPopover.swift b/DuckDuckGo/Autofill/ContentOverlayPopover.swift index 6d1448ef9e..cb3cfaef6e 100644 --- a/DuckDuckGo/Autofill/ContentOverlayPopover.swift +++ b/DuckDuckGo/Autofill/ContentOverlayPopover.swift @@ -21,13 +21,13 @@ import WebKit import BrowserServicesKit public final class ContentOverlayPopover { - + public var zoomFactor: CGFloat? public weak var currentTabView: NSView? public var viewController: ContentOverlayViewController public var windowController: NSWindowController - + public init(currentTabView: NSView) { let storyboard = NSStoryboard(name: "ContentOverlay", bundle: Bundle.main) viewController = storyboard.instantiateController(identifier: "ContentOverlayViewController") @@ -36,7 +36,7 @@ public final class ContentOverlayPopover { windowController.window?.hasShadow = true windowController.window?.acceptsMouseMovedEvents = true windowController.window?.ignoresMouseEvents = false - + viewController.view.wantsLayer = true if let layer = viewController.view.layer { layer.masksToBounds = true diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 1b97d05b82..d62f7f851e 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -144,7 +144,6 @@ public final class ContentOverlayViewController: NSViewController, EmailManagerR // EmailManagerRequestDelegate - // swiftlint:disable function_parameter_count public func emailManager(_ emailManager: EmailManager, requested url: URL, method: String, @@ -168,23 +167,23 @@ public final class ContentOverlayViewController: NSViewController, EmailManagerR }.resume() } // swiftlint:enable function_parameter_count - + public func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) { var parameters = [ "access_type": accessType.rawValue, "error": error.errorDescription ] - + if case let .keychainLookupFailure(status) = error { parameters["keychain_status"] = String(status) parameters["keychain_operation"] = "lookup" } - + if case let .keychainDeleteFailure(status) = error { parameters["keychain_status"] = String(status) parameters["keychain_operation"] = "delete" } - + if case let .keychainSaveFailure(status) = error { parameters["keychain_status"] = String(status) parameters["keychain_operation"] = "save" @@ -218,7 +217,7 @@ extension ContentOverlayViewController: OverlayAutofillUserScriptPresentationDel } extension ContentOverlayViewController: SecureVaultManagerDelegate { - + public func secureVaultManagerIsEnabledStatus(_: SecureVaultManager) -> Bool { return true } @@ -226,7 +225,7 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { public func secureVaultManager(_: SecureVaultManager, promptUserToStoreAutofillData data: AutofillData) { // No-op, the content overlay view controller should not be prompting the user to store data } - + public func secureVaultManager(_: SecureVaultManager, promptUserToAutofillCredentialsForDomain domain: String, withAccounts accounts: [SecureVaultModels.WebsiteAccount], @@ -238,11 +237,11 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { public func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: String) { Pixel.fire(.formAutofilled(kind: type.formAutofillKind)) } - + public func secureVaultManagerShouldAutomaticallyUpdateCredentialsWithoutUsername(_: SecureVaultManager) -> Bool { return true } - + public func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler handler: @escaping (Bool) -> Void) { DeviceAuthenticator.shared.authenticateUser(reason: .autofill) { authenticationResult in handler(authenticationResult.authenticated) @@ -252,7 +251,7 @@ extension ContentOverlayViewController: SecureVaultManagerDelegate { public func secureVaultInitFailed(_ error: SecureVaultError) { SecureVaultErrorReporter.shared.secureVaultInitFailed(error) } - + public func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, didReceivePixel pixel: AutofillUserScript.JSPixel) { Pixel.fire(.jsPixel(pixel)) } diff --git a/DuckDuckGo/Bookmarks Bar/View/BookmarksBarCollectionViewItem.swift b/DuckDuckGo/Bookmarks Bar/View/BookmarksBarCollectionViewItem.swift index a9b7441e0d..13670725f5 100644 --- a/DuckDuckGo/Bookmarks Bar/View/BookmarksBarCollectionViewItem.swift +++ b/DuckDuckGo/Bookmarks Bar/View/BookmarksBarCollectionViewItem.swift @@ -21,7 +21,7 @@ import Cocoa protocol BookmarksBarCollectionViewItemDelegate: AnyObject { func bookmarksBarCollectionViewItemClicked(_ item: BookmarksBarCollectionViewItem) - + func bookmarksBarCollectionViewItemOpenInNewTabAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemOpenInNewWindowAction(_ item: BookmarksBarCollectionViewItem) func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) @@ -33,7 +33,7 @@ protocol BookmarksBarCollectionViewItemDelegate: AnyObject { } final class BookmarksBarCollectionViewItem: NSCollectionViewItem { - + static let identifier = NSUserInterfaceItemIdentifier(rawValue: "BookmarksBarCollectionViewItem") @IBOutlet var stackView: NSStackView! @@ -50,11 +50,11 @@ final class BookmarksBarCollectionViewItem: NSCollectionViewItem { mouseClickView.delegate = self } } - + private enum EntityType { case bookmark(title: String, url: URL, favicon: NSImage?, isFavorite: Bool) case folder(title: String) - + var isFolder: Bool { switch self { case .bookmark: return false @@ -65,26 +65,26 @@ final class BookmarksBarCollectionViewItem: NSCollectionViewItem { weak var delegate: BookmarksBarCollectionViewItemDelegate? private var entityType: EntityType? - + /// MouseClickView is prone to sending mouseUp events without a preceding mouseDown. /// This tracks whether to consider a click as legitimate and use it to trigger navigation from the bookmarks bar. private var receivedMouseDownEvent = false - + override func viewDidLoad() { super.viewDidLoad() configureLayer() createMenu() } - + override func viewDidLayout() { super.viewDidLayout() mouseOverView.updateTrackingAreas() } - + func updateItem(from entity: BaseBookmarkEntity) { self.title = entity.title - + if let bookmark = entity as? Bookmark { let favicon = bookmark.favicon(.small)?.copy() as? NSImage favicon?.size = NSSize.faviconSize @@ -95,14 +95,14 @@ final class BookmarksBarCollectionViewItem: NSCollectionViewItem { } else { fatalError("Could not cast bookmark subclass from entity") } - + guard let entityType = entityType else { assertionFailure("Failed to get entity type") return } - + self.titleLabel.stringValue = entity.title - + switch entityType { case .bookmark(_, let url, let storedFavicon, _): let favicon = storedFavicon ?? FaviconManager.shared.getCachedFavicon(for: url.host ?? "", sizeCategory: .small)?.image @@ -111,23 +111,23 @@ final class BookmarksBarCollectionViewItem: NSCollectionViewItem { faviconView.image = NSImage(named: "Folder-16") } } - + private func configureLayer() { view.wantsLayer = true view.layer?.cornerRadius = 4.0 view.layer?.masksToBounds = true } - + private func createMenu() { let menu = NSMenu() menu.delegate = self view.menu = menu } - + } extension BookmarksBarCollectionViewItem: MouseClickViewDelegate { - + func mouseClickView(_ mouseClickView: MouseClickView, mouseDownEvent: NSEvent) { receivedMouseDownEvent = true } @@ -146,14 +146,14 @@ extension BookmarksBarCollectionViewItem: MouseClickViewDelegate { // MARK: - NSMenu extension BookmarksBarCollectionViewItem: NSMenuDelegate { - + func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - + guard let entityType = entityType else { return } - + switch entityType { case .bookmark(_, _, _, let isFavorite): menu.items = createBookmarkMenuItems(isFavorite: isFavorite) @@ -161,13 +161,13 @@ extension BookmarksBarCollectionViewItem: NSMenuDelegate { menu.items = createFolderMenuItems() } } - + } extension BookmarksBarCollectionViewItem { - + // MARK: Bookmark Menu Items - + func createBookmarkMenuItems(isFavorite: Bool) -> [NSMenuItem] { let items = [ openBookmarkInNewTabMenuItem(), @@ -180,14 +180,14 @@ extension BookmarksBarCollectionViewItem { copyBookmarkURLMenuItem(), deleteEntityMenuItem() ].compactMap { $0 } - + return items } - + func openBookmarkInNewTabMenuItem() -> NSMenuItem { return menuItem(UserText.openInNewTab, #selector(openBookmarkInNewTabMenuItemSelected(_:))) } - + @objc func openBookmarkInNewTabMenuItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewTabAction(self) @@ -196,12 +196,12 @@ extension BookmarksBarCollectionViewItem { func openBookmarkInNewWindowMenuItem() -> NSMenuItem { return menuItem(UserText.openInNewWindow, #selector(openBookmarkInNewWindowMenuItemSelected(_:))) } - + @objc func openBookmarkInNewWindowMenuItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemOpenInNewWindowAction(self) } - + func addToFavoritesMenuItem(isFavorite: Bool) -> NSMenuItem? { guard !isFavorite else { return nil @@ -209,50 +209,50 @@ extension BookmarksBarCollectionViewItem { return menuItem(UserText.addToFavorites, #selector(addToFavoritesMenuItemSelected(_:))) } - + @objc func addToFavoritesMenuItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemAddToFavoritesAction(self) } - + func editItem() -> NSMenuItem { return menuItem("Edit…", #selector(editItemSelected(_:))) } - + @objc func editItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewEditAction(self) } - + func moveToEndMenuItem() -> NSMenuItem { return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(moveToEndMenuItemSelected(_:))) } - + @objc func moveToEndMenuItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemMoveToEndAction(self) } - + func copyBookmarkURLMenuItem() -> NSMenuItem { return menuItem(UserText.bookmarksBarContextMenuCopy, #selector(copyBookmarkURLMenuItemSelected(_:))) } - + @objc func copyBookmarkURLMenuItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemCopyBookmarkURLAction(self) } - + func deleteEntityMenuItem() -> NSMenuItem { return menuItem(UserText.bookmarksBarContextMenuDelete, #selector(deleteMenuItemSelected(_:))) } - + @objc func deleteMenuItemSelected(_ sender: NSMenuItem) { delegate?.bookmarksBarCollectionViewItemDeleteEntityAction(self) } - + // MARK: Folder Menu Items - + func createFolderMenuItems() -> [NSMenuItem] { return [ editItem(), @@ -261,9 +261,9 @@ extension BookmarksBarCollectionViewItem { deleteEntityMenuItem() ] } - + func menuItem(_ title: String, _ action: Selector) -> NSMenuItem { return NSMenuItem(title: title, action: action, keyEquivalent: "") } - + } diff --git a/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewController.swift b/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewController.swift index a075994a6a..b7ce95c82d 100644 --- a/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewController.swift @@ -29,32 +29,32 @@ final class BookmarksBarViewController: NSViewController { private let bookmarkManager = LocalBookmarkManager.shared private let viewModel: BookmarksBarViewModel private let tabCollectionViewModel: TabCollectionViewModel - + private var viewModelCancellable: AnyCancellable? - + fileprivate var clipThreshold: CGFloat { let viewWidthWithoutClipIndicator = view.frame.width - clippedItemsIndicator.frame.minX return view.frame.width - viewWidthWithoutClipIndicator - 3 } - + init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel) { self.tabCollectionViewModel = tabCollectionViewModel self.viewModel = BookmarksBarViewModel(bookmarkManager: LocalBookmarkManager.shared, tabCollectionViewModel: tabCollectionViewModel) super.init(coder: coder) } - + required init?(coder: NSCoder) { fatalError("TabBarViewController: Bad initializer") } - + // MARK: - View Lifecycle override func viewDidLoad() { super.viewDidLoad() addContextMenu() - + viewModel.delegate = self let nib = NSNib(nibNamed: "BookmarksBarCollectionViewItem", bundle: .main) @@ -66,31 +66,31 @@ final class BookmarksBarViewController: NSViewController { BookmarkPasteboardWriter.bookmarkUTIInternalType, FolderPasteboardWriter.folderUTIInternalType ]) - + bookmarksBarCollectionView.delegate = viewModel bookmarksBarCollectionView.dataSource = viewModel bookmarksBarCollectionView.collectionViewLayout = createCenteredCollectionViewLayout() - + view.postsFrameChangedNotifications = true } - + private func addContextMenu() { let menu = NSMenu() menu.delegate = self self.view.menu = menu } - + override func viewWillAppear() { super.viewWillAppear() subscribeToEvents() refreshFavicons() } - + override func viewDidAppear() { super.viewDidAppear() frameChangeNotification() } - + private func subscribeToViewModel() { guard viewModelCancellable.isNil else { assertionFailure("Tried to subscribe to view model while it is already subscribed") @@ -101,7 +101,7 @@ final class BookmarksBarViewController: NSViewController { self?.refreshClippedIndicator() } } - + @objc private func frameChangeNotification() { // Wait until the frame change has taken effect for subviews before calculating changes to the list of items. @@ -110,51 +110,51 @@ final class BookmarksBarViewController: NSViewController { self.refreshClippedIndicator() } } - + override func removeFromParent() { super.removeFromParent() unsubscribeFromEvents() } - + private func subscribeToEvents() { NotificationCenter.default.addObserver(self, selector: #selector(frameChangeNotification), name: NSView.frameDidChangeNotification, object: view) - + NotificationCenter.default.addObserver(self, selector: #selector(refreshFavicons), name: .faviconCacheUpdated, object: nil) - + subscribeToViewModel() } - + private func unsubscribeFromEvents() { NotificationCenter.default.removeObserver(self, name: NSView.frameDidChangeNotification, object: nil) NotificationCenter.default.removeObserver(self, name: .faviconCacheUpdated, object: nil) - + viewModelCancellable?.cancel() viewModelCancellable = nil } - + // MARK: - Layout - + private func createCenteredLayout(centered: Bool) -> NSCollectionLayoutSection { let group = NSCollectionLayoutGroup.horizontallyCentered(cellSizes: viewModel.cellSizes, centered: centered) return NSCollectionLayoutSection(group: group) } - + func createCenteredCollectionViewLayout() -> NSCollectionViewLayout { return NSCollectionViewCompositionalLayout { [unowned self] _, _ in return createCenteredLayout(centered: viewModel.clippedItems.isEmpty) } } - + private func refreshClippedIndicator() { self.clippedItemsIndicator.isHidden = viewModel.clippedItems.isEmpty } - + @objc private func refreshFavicons() { bookmarksBarCollectionView.reloadData() @@ -167,7 +167,7 @@ final class BookmarksBarViewController: NSViewController { menu.popUp(positioning: nil, at: location, in: sender) } - + } extension BookmarksBarViewController: BookmarksBarViewModelDelegate { @@ -177,12 +177,12 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { assertionFailure("Failed to look up index path for clicked item") return } - + guard let entity = bookmarkManager.list?.topLevelEntities[indexPath.item] else { assertionFailure("Failed to get entity for clicked item") return } - + if let bookmark = entity as? Bookmark { handle(action, for: bookmark) } else if let folder = entity as? BookmarkFolder { @@ -191,15 +191,15 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { assertionFailure("Failed to cast entity for clicked item") } } - + func bookmarksBarViewModelWidthForContainer() -> CGFloat { return clipThreshold } - + func bookmarksBarViewModelReloadedData() { bookmarksBarCollectionView.reloadData() } - + private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for bookmark: Bookmark) { switch action { case .openInNewTab: @@ -224,7 +224,7 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { bookmarkManager.remove(bookmark: bookmark) } } - + private func handle(_ action: BookmarksBarViewModel.BookmarksBarItemAction, for folder: BookmarkFolder, item: BookmarksBarCollectionViewItem) { switch action { case .clickItem: @@ -247,29 +247,29 @@ extension BookmarksBarViewController: BookmarksBarViewModelDelegate { assertionFailure("Received unexpected action for bookmark folder") } } - + private func bookmarkFolderMenu(items: [NSMenuItem]) -> NSMenu { let menu = NSMenu() menu.items = items.isEmpty ? [NSMenuItem.empty] : items return menu } - + } // MARK: - Menu extension BookmarksBarViewController: NSMenuDelegate { - + public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - + if PersistentAppInterfaceSettings.shared.showBookmarksBar { menu.addItem(withTitle: UserText.hideBookmarksBar, action: #selector(toggleBookmarksBar), keyEquivalent: "") } else { menu.addItem(withTitle: UserText.showBookmarksBar, action: #selector(toggleBookmarksBar), keyEquivalent: "") } } - + @objc private func toggleBookmarksBar(_ sender: NSMenuItem) { PersistentAppInterfaceSettings.shared.showBookmarksBar.toggle() @@ -284,18 +284,18 @@ extension BookmarksBarViewController: AddBookmarkModalViewControllerDelegate, Ad func addFolderViewController(_ viewController: AddFolderModalViewController, addedFolderWith name: String) { assertionFailure("Cannot add new folders to the bookmarks bar via the modal") } - + func addFolderViewController(_ viewController: AddFolderModalViewController, saved folder: BookmarkFolder) { bookmarkManager.update(folder: folder) } - + func addBookmarkViewController(_ viewController: AddBookmarkModalViewController, addedBookmarkWithTitle title: String, url: URL) { assertionFailure("Cannot add new bookmarks to the bookmarks bar via the modal") } - + func addBookmarkViewController(_ viewController: AddBookmarkModalViewController, saved bookmark: Bookmark, newURL: URL) { bookmarkManager.update(bookmark: bookmark) _ = bookmarkManager.updateUrl(of: bookmark, to: newURL) } - + } diff --git a/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewModel.swift b/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewModel.swift index e0e072c76e..42e8a04dd4 100644 --- a/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewModel.swift +++ b/DuckDuckGo/Bookmarks Bar/View/BookmarksBarViewModel.swift @@ -21,28 +21,28 @@ import Combine import Foundation protocol BookmarksBarViewModelDelegate: AnyObject { - + func bookmarksBarViewModelReceived(action: BookmarksBarViewModel.BookmarksBarItemAction, for item: BookmarksBarCollectionViewItem) func bookmarksBarViewModelWidthForContainer() -> CGFloat func bookmarksBarViewModelReloadedData() - + } final class BookmarksBarViewModel: NSObject { - + // MARK: Enums - + enum Constants { static let buttonSpacing: CGFloat = 6 static let buttonHeight: CGFloat = 28 static let maximumButtonWidth: CGFloat = 120 static let labelFont = NSFont.systemFont(ofSize: 12) - + static let additionalItemWidth = 34.0 - + static let interItemGapIndicatorIdentifier = "NSCollectionElementKindInterItemGapIndicator" } - + enum BookmarksBarItemAction { case clickItem case openInNewTab @@ -53,13 +53,13 @@ final class BookmarksBarViewModel: NSObject { case copyURL case deleteEntity } - + struct BookmarksBarItem { let title: String let url: URL? let isFolder: Bool let entity: BaseBookmarkEntity - + init(entity: BaseBookmarkEntity) { self.title = entity.title self.url = (entity as? Bookmark)?.url @@ -67,27 +67,27 @@ final class BookmarksBarViewModel: NSObject { self.entity = entity } } - + weak var delegate: BookmarksBarViewModelDelegate? private let bookmarkManager: BookmarkManager private let tabCollectionViewModel: TabCollectionViewModel private var cancellables = Set() - + private var existingItemDraggingIndexPath: IndexPath? private var preventClicks = false private var collectionViewItemSizeCache: [String: CGFloat] = [:] private var bookmarksBarItemsTotalWidth: CGFloat = 0 - + private let textSizeCalculationLabel: NSTextField = { let calculationLabel = NSTextField.label(titled: "") calculationLabel.font = Constants.labelFont calculationLabel.lineBreakMode = .byTruncatingMiddle - + return calculationLabel }() - + private var bookmarksBarItems: [BookmarksBarItem] = [] { didSet { let itemsWidth = bookmarksBarItems.reduce(CGFloat(0)) { total, item in @@ -104,7 +104,7 @@ final class BookmarksBarViewModel: NSObject { @Published private(set) var clippedItems: [BookmarkViewModel] = [] - + var cellSizes: [CGSize] { let widths = bookmarksBarItems.map { item in return cachedWidth(buttonTitle: item.title) @@ -112,16 +112,16 @@ final class BookmarksBarViewModel: NSObject { return widths.map { CGSize(width: $0, height: Constants.buttonHeight) } } - + // MARK: - Initialization - + init(bookmarkManager: BookmarkManager, tabCollectionViewModel: TabCollectionViewModel) { self.bookmarkManager = bookmarkManager self.tabCollectionViewModel = tabCollectionViewModel super.init() subscribeToBookmarks() } - + private func subscribeToBookmarks() { bookmarkManager.listPublisher.receive(on: RunLoop.main).sink { [weak self] list in let containerWidth = self?.delegate?.bookmarksBarViewModelWidthForContainer() ?? 0 @@ -129,9 +129,9 @@ final class BookmarksBarViewModel: NSObject { self?.delegate?.bookmarksBarViewModelReloadedData() }.store(in: &cancellables) } - + // MARK: - Functions - + func update(from bookmarkEntities: [BaseBookmarkEntity], containerWidth: CGFloat) { clippedItems = [] @@ -141,7 +141,7 @@ final class BookmarksBarViewModel: NSObject { for (index, entity) in bookmarkEntities.enumerated() { let calculatedWidth = self.cachedWidth(buttonTitle: entity.title) - + if currentTotalWidth == 0 { currentTotalWidth += calculatedWidth } else { @@ -152,28 +152,28 @@ final class BookmarksBarViewModel: NSObject { clippedItemsStartingIndex = index break } - + let item = BookmarksBarItem(entity: entity) displayableItems.append(item) } - + self.bookmarksBarItems = displayableItems - + if let clippedItemsStartingIndex = clippedItemsStartingIndex { let clippedEntities = bookmarkEntities[clippedItemsStartingIndex...] - + for clippedEntity in clippedEntities { clippedItems.append(BookmarkViewModel(entity: clippedEntity)) } } } - + func clipOrRestoreBookmarksBarItems() { guard let clipThreshold = delegate?.bookmarksBarViewModelWidthForContainer() else { assertionFailure("Failed to get width of bookmarks bar container") return } - + guard !bookmarksBarItems.isEmpty else { return } @@ -194,16 +194,16 @@ final class BookmarksBarViewModel: NSObject { if !restoreNextClippedItemToBookmarksBarIfPossible(item: nextRestorableClippedItem) { break } - + restoredItem = true } - + if restoredItem { delegate?.bookmarksBarViewModelReloadedData() } } } - + private func restoreNextClippedItemToBookmarksBarIfPossible(item: BookmarkViewModel) -> Bool { guard let clipThreshold = delegate?.bookmarksBarViewModelWidthForContainer() else { assertionFailure("Failed to get width of bookmarks bar container") @@ -216,55 +216,55 @@ final class BookmarksBarViewModel: NSObject { if newMaximumWidth < clipThreshold { return restoreLastClippedItem() } - + return false } - + func cachedWidth(buttonTitle: String) -> CGFloat { if let cachedValue = collectionViewItemSizeCache[buttonTitle] { return cachedValue + Constants.additionalItemWidth - } else { + } else { textSizeCalculationLabel.stringValue = buttonTitle textSizeCalculationLabel.sizeToFit() let cappedTitleWidth = ceil(min(Constants.maximumButtonWidth, textSizeCalculationLabel.frame.width)) let calculatedWidth = min(Constants.maximumButtonWidth, textSizeCalculationLabel.frame.width) + Constants.additionalItemWidth collectionViewItemSizeCache[buttonTitle] = cappedTitleWidth - + return ceil(calculatedWidth) } } - + func clipLastBarItem() -> Bool { guard let poppedItem = bookmarksBarItems.popLast() else { return false } - + let viewModel = BookmarkViewModel(entity: poppedItem.entity) clippedItems.insert(viewModel, at: 0) - + return true } - + func restoreLastClippedItem() -> Bool { guard !clippedItems.isEmpty else { return false } - + let item = clippedItems.removeFirst() let bookmarksBarItem = BookmarksBarItem(entity: item.entity) - + bookmarksBarItems.append(bookmarksBarItem) - + return true } - + func buildClippedItemsMenu() -> NSMenu { let menu = NSMenu() menu.items = bookmarksTreeMenuItems(from: clippedItems) return menu } - + func bookmarksTreeMenuItems(from bookmarkViewModels: [BookmarkViewModel], topLevel: Bool = true) -> [NSMenuItem] { var menuItems = [NSMenuItem]() @@ -290,10 +290,10 @@ final class BookmarksBarViewModel: NSObject { menuItems.append(.separator()) menuItems.append(NSMenuItem(bookmarkViewModels: bookmarkViewModels)) } - + return menuItems } - + } extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataSource { @@ -309,28 +309,28 @@ extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataS let image = NSImage(named: "Drop-Target-Indicator-16")! let imageView = NSImageView(image: image) imageView.contentTintColor = NSColor.controlAccentColor - + return imageView } func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return bookmarksBarItems.count } - + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let genericCollectionViewItem = collectionView.makeItem(withIdentifier: BookmarksBarCollectionViewItem.identifier, for: indexPath) - + guard let bookmarksCollectionViewItem = genericCollectionViewItem as? BookmarksBarCollectionViewItem else { return genericCollectionViewItem } - + let bookmarksBarItem = bookmarksBarItems[indexPath.item] bookmarksCollectionViewItem.delegate = self bookmarksCollectionViewItem.updateItem(from: bookmarksBarItem.entity) - + return bookmarksCollectionViewItem } - + func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, @@ -338,22 +338,22 @@ extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataS assert(indexPaths.count == 1) // Only one item can be dragged from the bar at a time self.existingItemDraggingIndexPath = indexPaths.first } - + func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) { self.existingItemDraggingIndexPath = nil } - + func collectionView(_ collectionView: NSCollectionView, canDragItemsAt indexPaths: Set, with event: NSEvent) -> Bool { return true } - + func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { return bookmarksBarItems[indexPath.item].entity.pasteboardWriter } - + func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, @@ -361,14 +361,14 @@ extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataS if proposedDropOperation.pointee == .on { proposedDropOperation.pointee = .before } - + if existingItemDraggingIndexPath != nil { return .move } else { return .copy } } - + func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath newIndexPath: IndexPath, @@ -377,9 +377,9 @@ extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataS if let existingIndexPath = existingItemDraggingIndexPath { let entityUUID = self.bookmarksBarItems[existingIndexPath.item].entity.id - + let index: Int - + if existingIndexPath.item <= newIndexPath.item { index = newIndexPath.item - 1 } else { @@ -389,7 +389,7 @@ extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataS self.bookmarksBarItems.move(fromOffsets: IndexSet(integer: existingIndexPath.item), toOffset: newIndexPath.item) collectionView.animator().moveItem(at: existingIndexPath, to: IndexPath(item: index, section: 0)) existingItemDraggingIndexPath = nil - + bookmarkManager.move(objectUUIDs: [entityUUID], toIndex: newIndexPath.item, withinParentFolder: .root) { error in if error != nil { self.delegate?.bookmarksBarViewModelReloadedData() @@ -421,28 +421,28 @@ extension BookmarksBarViewModel: NSCollectionViewDelegate, NSCollectionViewDataS self.bookmarkManager.makeBookmark(for: draggedURL, title: title, isFavorite: false, index: currentIndexPathItem, parent: nil) } - + currentIndexPathItem += 1 } - + return true } - + return false } - + // MARK: - Drag & Drop /// On rare occasions, a click event will be sent immediately after a drag and drop operation completes. /// To prevent drag and drop from accidentally triggering a bookmark to load or folder to open, all click events are ignored for a short period after a drop has been accepted. private func beginClickPreventionTimer() { preventClicks = true - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { self.preventClicks = false } } - + } extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { @@ -454,33 +454,33 @@ extension BookmarksBarViewModel: BookmarksBarCollectionViewItemDelegate { delegate?.bookmarksBarViewModelReceived(action: .clickItem, for: item) } - - func bookmarksBarCollectionViewItemOpenInNewTabAction(_ item: BookmarksBarCollectionViewItem) { + + func bookmarksBarCollectionViewItemOpenInNewTabAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .openInNewTab, for: item) } - + func bookmarksBarCollectionViewItemOpenInNewWindowAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .openInNewWindow, for: item) } - + func bookmarksBarCollectionViewItemAddToFavoritesAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .addToFavorites, for: item) } - + func bookmarksBarCollectionViewEditAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .edit, for: item) } - + func bookmarksBarCollectionViewItemMoveToEndAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .moveToEnd, for: item) } - + func bookmarksBarCollectionViewItemCopyBookmarkURLAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .copyURL, for: item) } - + func bookmarksBarCollectionViewItemDeleteEntityAction(_ item: BookmarksBarCollectionViewItem) { delegate?.bookmarksBarViewModelReceived(action: .deleteEntity, for: item) } - + } diff --git a/DuckDuckGo/Bookmarks Bar/View/HorizontallyCenteredLayout.swift b/DuckDuckGo/Bookmarks Bar/View/HorizontallyCenteredLayout.swift index 9940a301e1..c03afaa6a9 100644 --- a/DuckDuckGo/Bookmarks Bar/View/HorizontallyCenteredLayout.swift +++ b/DuckDuckGo/Bookmarks Bar/View/HorizontallyCenteredLayout.swift @@ -21,7 +21,7 @@ extension NSCollectionLayoutGroup { static func horizontallyCentered(cellSizes: [CGSize], interItemSpacing: CGFloat = 6, centered: Bool = true) -> NSCollectionLayoutGroup { let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(28)) - + return custom(layoutSize: groupSize) { environment in let verticalPosition: CGFloat = environment.container.contentInsets.top let totalWidth = cellSizes.map(\.width).reduce(0) { $0 == 0 ? $1 : $0 + interItemSpacing + $1 } @@ -29,9 +29,9 @@ extension NSCollectionLayoutGroup { var items: [NSCollectionLayoutGroupCustomItem] = [] var horizontalPosition: CGFloat - + // Derive initial horizontal position: - + if centered { horizontalPosition = (environment.container.effectiveContentSize.width - totalWidth) / 2 + environment.container.contentInsets.leading } else { @@ -39,7 +39,7 @@ extension NSCollectionLayoutGroup { } // Calculate frames for layout group items: - + let rowItems: [NSCollectionLayoutGroupCustomItem] = cellSizes.map { size in let origin = CGPoint(x: horizontalPosition, y: verticalPosition + (maxItemHeight - size.height) / 2) let itemFrame = CGRect(origin: origin, size: size) @@ -47,9 +47,9 @@ extension NSCollectionLayoutGroup { return NSCollectionLayoutGroupCustomItem(frame: itemFrame) } - + items.append(contentsOf: rowItems) - + return items } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index 4929ca50cc..c0bd66c55f 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -21,13 +21,13 @@ import BrowserServicesKit import os.log struct BookmarkList { - + struct IdentifiableBookmark: Equatable, BrowserServicesKit.Bookmark { let id: UUID let url: URL let title: String let isFavorite: Bool - + init(from bookmark: Bookmark) { self.id = bookmark.id self.url = bookmark.url @@ -51,10 +51,10 @@ struct BookmarkList { guard let array = itemsDict[favoriteBookmark.url] else { return nil } - + return array.first(where: { $0.id == favoriteBookmark.id }) } - + return bookmarks } @@ -63,11 +63,11 @@ struct BookmarkList { let keysOrdered = bookmarks.compactMap { IdentifiableBookmark(from: $0) } var itemsDict = [URL: [Bookmark]]() - + for bookmark in bookmarks { itemsDict[bookmark.url] = (itemsDict[bookmark.url] ?? []) + [bookmark] } - + self.favoriteBookmarksOrdered = favorites.compactMap({$0 as? Bookmark}).map(IdentifiableBookmark.init(from:)) self.allBookmarkURLsOrdered = keysOrdered self.itemsDict = itemsDict @@ -90,10 +90,10 @@ struct BookmarkList { mutating func remove(_ bookmark: Bookmark) { allBookmarkURLsOrdered.removeAll { $0.id == bookmark.id } - + let existingBookmarks = itemsDict[bookmark.url] ?? [] let updatedBookmarks = existingBookmarks.filter { $0.id != bookmark.id } - + if updatedBookmarks.isEmpty { itemsDict[bookmark.url] = nil } else { @@ -136,16 +136,16 @@ struct BookmarkList { let newBookmark = Bookmark(from: bookmark, with: newURL) let newIdentifiableBookmark = IdentifiableBookmark(from: newBookmark) - + allBookmarkURLsOrdered.remove(at: index) allBookmarkURLsOrdered.insert(newIdentifiableBookmark, at: index) - + let existingBookmarks = itemsDict[bookmark.url] ?? [] let updatedBookmarks = existingBookmarks.filter { $0.id != bookmark.id } - + itemsDict[bookmark.url] = updatedBookmarks itemsDict[newURL] = (itemsDict[newURL] ?? []) + [bookmark] - + return newBookmark } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManagedObject.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManagedObject.swift index 7ec7c17571..7f6bd3ffdc 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManagedObject.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManagedObject.swift @@ -67,7 +67,7 @@ extension BookmarkManagedObject { try validateThatFolderHierarchyHasNoCycles() try validateFavoritesFolder() } - + func validateThatEntitiesExistInsideTheRootFolder() throws { if parentFolder == nil, ![UUID.rootBookmarkFolderUUID, .favoritesFolderUUID].contains(id) { throw BookmarkError.mustExistInsideRootFolder diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index 9904974be5..30827b5ecb 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -114,7 +114,7 @@ final class LocalBookmarkManager: BookmarkManager { @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool) -> Bookmark? { makeBookmark(for: url, title: title, isFavorite: isFavorite, index: nil, parent: nil) } - + @discardableResult func makeBookmark(for url: URL, title: String, isFavorite: Bool, index: Int? = nil, parent: BookmarkFolder? = nil) -> Bookmark? { guard list != nil else { return nil } @@ -161,7 +161,7 @@ final class LocalBookmarkManager: BookmarkManager { self?.loadBookmarks() } } - + func remove(objectsWithUUIDs uuids: [UUID]) { bookmarkStore.remove(objectsWithUUIDs: uuids) { [weak self] _, _ in self?.loadBookmarks() @@ -222,7 +222,7 @@ final class LocalBookmarkManager: BookmarkManager { func add(bookmark: Bookmark, to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) { add(objectsWithUUIDs: [bookmark.id], to: parent, completion: completion) } - + func add(objectsWithUUIDs uuids: [UUID], to parent: BookmarkFolder?, completion: @escaping (Error?) -> Void) { bookmarkStore.add(objectsWithUUIDs: uuids, to: parent) { [weak self] error in self?.loadBookmarks() @@ -236,11 +236,11 @@ final class LocalBookmarkManager: BookmarkManager { completion(error) } } - + func canMoveObjectWithUUID(objectUUID uuid: UUID, to parent: BookmarkFolder) -> Bool { return bookmarkStore.canMoveObjectWithUUID(objectUUID: uuid, to: parent) } - + func move(objectUUIDs: [UUID], toIndex index: Int?, withinParentFolder parent: ParentFolderType, completion: @escaping (Error?) -> Void) { bookmarkStore.move(objectUUIDs: objectUUIDs, toIndex: index, withinParentFolder: parent) { [weak self] error in self?.loadBookmarks() @@ -273,14 +273,14 @@ final class LocalBookmarkManager: BookmarkManager { return results } - + // MARK: - Debugging - + func resetBookmarks() { guard let store = bookmarkStore as? LocalBookmarkStore else { return } - + store.resetBookmarks() loadBookmarks() } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index 6b9a3f8b0a..96986a900a 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -153,16 +153,16 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS if contentMode == .foldersOnly, index != -1 { return .none } - + let destinationNode = nodeForItem(item) - + let bookmarks = PasteboardBookmark.pasteboardBookmarks(with: info.draggingPasteboard) let folders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard) if let bookmarks = bookmarks, let folders = folders { let canMoveBookmarks = validateDrop(for: bookmarks, destination: destinationNode) == .move let canMoveFolders = validateDrop(for: folders, destination: destinationNode) == .move - + // If the dragged values contain both folders and bookmarks, only validate the move if all objects can be moved. if canMoveBookmarks, canMoveFolders { return .move @@ -239,7 +239,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS !draggedObjectIdentifiers.isEmpty else { return false } - + let representedObject = (item as? BookmarkNode)?.representedObject // Handle the nil destination case: @@ -273,7 +273,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS os_log("Failed to accept existing parent drop via outline view: %s", error.localizedDescription) } } - + return true } else if representedObject == nil { bookmarkManager.move(objectUUIDs: draggedObjectIdentifiers, toIndex: index, withinParentFolder: .root) { error in @@ -281,7 +281,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS os_log("Failed to accept existing parent drop via outline view: %s", error.localizedDescription) } } - + return true } else { return false diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index 0b4affee1d..8628b0c4cb 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -63,7 +63,7 @@ protocol BookmarkStore { // swiftlint:disable:next type_body_length final class LocalBookmarkStore: BookmarkStore { - + enum Constants { static let rootFolderUUID = "87E09C05-17CB-4185-9EDF-8D1AF4312BAF" static let favoritesFolderUUID = "23B11CC4-EA56-4937-8EC1-15854109AE35" @@ -77,7 +77,7 @@ final class LocalBookmarkStore: BookmarkStore { self.context = context sharedInitialization() } - + private func sharedInitialization() { removeInvalidBookmarkEntities() migrateTopLevelStorageToRootLevelBookmarksFolder() @@ -94,7 +94,7 @@ final class LocalBookmarkStore: BookmarkStore { } private lazy var context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "Bookmark") - + /// All entities within the bookmarks store must exist under this root level folder. Because this value is used so frequently, it is cached here. private var rootLevelFolder: BookmarkManagedObject? @@ -109,17 +109,17 @@ final class LocalBookmarkStore: BookmarkStore { Constants.rootFolderUUID, #keyPath(BookmarkManagedObject.parentFolder), #keyPath(BookmarkManagedObject.isFolder)) - + let results = (try? context.fetch(fetchRequest)) ?? [] guard results.count == 1 else { fatalError("There shouldn't be more than one root folder") } - + guard let folder = results.first else { fatalError("Top level folder missing") } - + self.rootLevelFolder = folder } } @@ -149,7 +149,7 @@ final class LocalBookmarkStore: BookmarkStore { do { let results: [BookmarkManagedObject] - + switch type { case .bookmarks: results = try self.context.fetch(fetchRequest) @@ -162,7 +162,7 @@ final class LocalBookmarkStore: BookmarkStore { let entities = try self.context.fetch(fetchRequest) results = entities.first?.favorites?.array as? [BookmarkManagedObject] ?? [] } - + let entities: [BaseBookmarkEntity] = results.compactMap { entity in BaseBookmarkEntity.from(managedObject: entity, parentFolderUUID: entity.parentFolder?.id) } @@ -200,7 +200,7 @@ final class LocalBookmarkStore: BookmarkStore { let parentFetchRequest = BaseBookmarkEntity.singleEntity(with: parent.id) let parentFetchRequestResults = try? self.context.fetch(parentFetchRequest) let parentFolder = parentFetchRequestResults?.first - + if let index = index { parentFolder?.mutableChildren.insert(bookmarkMO, at: index) } else { @@ -428,37 +428,37 @@ final class LocalBookmarkStore: BookmarkStore { DispatchQueue.main.async { completion(true, nil) } } } - + func canMoveObjectWithUUID(objectUUID uuid: UUID, to parent: BookmarkFolder) -> Bool { guard uuid != parent.id else { // A folder cannot set itself as its parent return false } - + // Assume true by default – the database validations will serve as a final check before any invalid state makes it into the database. var canMoveObject = true - + context.performAndWait { [weak self] in guard let self = self else { assertionFailure("Couldn't get strong self") return } - + let folderToMoveFetchRequest = BaseBookmarkEntity.singleEntity(with: uuid) let folderToMoveFetchRequestResults = try? self.context.fetch(folderToMoveFetchRequest) - + let parentFolderFetchRequest = BaseBookmarkEntity.singleEntity(with: parent.id) let parentFolderFetchRequestResults = try? self.context.fetch(parentFolderFetchRequest) - + guard let folderToMove = folderToMoveFetchRequestResults?.first as? BookmarkManagedObject, let parentFolder = parentFolderFetchRequestResults?.first as? BookmarkManagedObject, folderToMove.isFolder, parentFolder.isFolder else { return } - + var currentParentFolder: BookmarkManagedObject? = parentFolder.parentFolder - + // Check each parent and verify that the folder being moved isn't being given itself as an ancestor while let currentParent = currentParentFolder { if currentParent.id == uuid { @@ -472,7 +472,7 @@ final class LocalBookmarkStore: BookmarkStore { return canMoveObject } - + func move(objectUUIDs: [UUID], toIndex index: Int?, withinParentFolder type: ParentFolderType, completion: @escaping (Error?) -> Void) { context.perform { [weak self] in guard let self = self else { @@ -480,27 +480,27 @@ final class LocalBookmarkStore: BookmarkStore { completion(nil) return } - + guard let rootFolder = self.rootLevelFolder else { assertionFailure("\(#file): Failed to get root level folder") completion(nil) return } - + // Guarantee that bookmarks are fetched in the same order as the UUIDs. In the future, this should fetch all objects at once with a // batch fetch request and have them sorted in the correct order. let bookmarkManagedObjects: [BookmarkManagedObject] = objectUUIDs.compactMap { uuid in let entityFetchRequest = BaseBookmarkEntity.singleEntity(with: uuid) return (try? self.context.fetch(entityFetchRequest))?.first } - + let newParentFolder: BookmarkManagedObject - + switch type { case .root: newParentFolder = rootFolder case .parent(let newParentUUID): let bookmarksFetchRequest = BaseBookmarkEntity.singleEntity(with: newParentUUID) - + do { if let fetchedParent = try self.context.fetch(bookmarksFetchRequest).first, fetchedParent.isFolder { newParentFolder = fetchedParent @@ -513,7 +513,7 @@ final class LocalBookmarkStore: BookmarkStore { return } } - + if let index = index, index < newParentFolder.mutableChildren.count { self.move(entities: bookmarkManagedObjects, to: index, within: newParentFolder) } else { @@ -535,21 +535,21 @@ final class LocalBookmarkStore: BookmarkStore { DispatchQueue.main.async { completion(nil) } } } - + private func move(entities bookmarkManagedObjects: [BookmarkManagedObject], to index: Int, within newParentFolder: BookmarkManagedObject) { var currentInsertionIndex = max(index, 0) - + for bookmarkManagedObject in bookmarkManagedObjects { let movingObjectWithinSameFolder = bookmarkManagedObject.parentFolder?.id == newParentFolder.id - + var adjustedInsertionIndex = currentInsertionIndex - + if movingObjectWithinSameFolder, currentInsertionIndex > newParentFolder.mutableChildren.index(of: bookmarkManagedObject) { adjustedInsertionIndex -= 1 } - + bookmarkManagedObject.parentFolder = nil - + // Removing the bookmark from its current parent may have removed it from the collection it is about to be added to, so re-check // the bounds before adding it back. if adjustedInsertionIndex < newParentFolder.mutableChildren.count { @@ -622,7 +622,7 @@ final class LocalBookmarkStore: BookmarkStore { } // MARK: - Import - + /// Imports bookmarks into the Core Data store from an `ImportedBookmarks` object. /// The source is used to determine where to put bookmarks, as we want to match the source browser's structure as closely as possible. /// @@ -678,7 +678,7 @@ final class LocalBookmarkStore: BookmarkStore { private func createEntitiesFromBookmarks(allFolders: [BookmarkManagedObject], bookmarks: ImportedBookmarks, importSourceName: String) -> BookmarkImportResult { - + var total = BookmarkImportResult(successful: 0, duplicates: 0, failed: 0) var parent: BookmarkManagedObject? @@ -700,12 +700,12 @@ final class LocalBookmarkStore: BookmarkStore { parent: parent, markBookmarksAsFavorite: false, in: self.context) - + total += result } - + return total - + } private func createFolder(titled title: String, in context: NSManagedObjectContext) -> BookmarkManagedObject { @@ -740,7 +740,7 @@ final class LocalBookmarkStore: BookmarkStore { bookmarkManagedObject.urlEncrypted = bookmarkOrFolder.url as NSURL? bookmarkManagedObject.dateAdded = NSDate.now bookmarkManagedObject.parentFolder = parent ?? self.rootLevelFolder - + // Bookmarks from the bookmarks bar are imported as favorites if bookmarkOrFolder.isDDGFavorite || (!bookmarkOrFolder.isFolder && markBookmarksAsFavorite == true) { bookmarkManagedObject.favoritesFolder = favoritesFolder @@ -771,29 +771,29 @@ final class LocalBookmarkStore: BookmarkStore { return total } - + // MARK: - Migration - + /// There is a rare issue where bookmark managed objects can end up in the database with an invalid state, that is that they are missing their title value despite being non-optional. /// They appear to be disjoint from a user's actual bookmarks data, so this function removes them. private func removeInvalidBookmarkEntities() { context.performAndWait { let entitiesFetchRequest = Bookmark.bookmarksAndFoldersFetchRequest() - + do { let entities = try self.context.fetch(entitiesFetchRequest) - + var deletedEntityCount = 0 - + for entity in entities where entity.isInvalid { self.context.delete(entity) deletedEntityCount += 1 } - + if deletedEntityCount > 0 { Pixel.fire(.debug(event: .removedInvalidBookmarkManagedObjects)) } - + try self.context.save() } catch { os_log("Failed to remove invalid bookmark entities", type: .error) @@ -806,19 +806,19 @@ final class LocalBookmarkStore: BookmarkStore { // 1. Fetch all top-level entities and check that there isn't an existing root folder // 2. If the root folder does not exist, create it // 3. Add all other top-level entities as children of the root folder - + let rootFolderFetchRequest = Bookmark.singleEntity(with: .rootBookmarkFolderUUID) - + let topLevelEntitiesFetchRequest = Bookmark.topLevelEntitiesFetchRequest() topLevelEntitiesFetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(BookmarkManagedObject.dateAdded), ascending: true)] topLevelEntitiesFetchRequest.returnsObjectsAsFaults = true - + do { // 0. Up front, check if a root folder exists but has been moved deeper into the hierarchy, and remove it if so: - + if let existingRootFolder = try self.context.fetch(rootFolderFetchRequest).first, let rootFolderParent = existingRootFolder.parentFolder { - + existingRootFolder.children?.forEach { child in if let bookmarkEntity = child as? BookmarkManagedObject { bookmarkEntity.parentFolder = rootFolderParent @@ -826,15 +826,15 @@ final class LocalBookmarkStore: BookmarkStore { assertionFailure("Tried to relocate child that was not a BookmarkManagedObject") } } - + // Since the existing root folder's children have been relocated, delete it and let it be recreated later. context.delete(existingRootFolder) } - + // 1. Get the existing top level entities and check for a root folder: - + var existingTopLevelEntities = try self.context.fetch(topLevelEntitiesFetchRequest) - + let existingTopLevelFolderIndex = existingTopLevelEntities.firstIndex { entity in entity.id == .rootBookmarkFolderUUID } @@ -845,9 +845,9 @@ final class LocalBookmarkStore: BookmarkStore { } // 2. Get or create the top level folder: - + let topLevelFolder: BookmarkManagedObject - + if let existingTopLevelFolderIndex = existingTopLevelFolderIndex { topLevelFolder = existingTopLevelEntities[existingTopLevelFolderIndex] existingTopLevelEntities.remove(at: existingTopLevelFolderIndex) @@ -874,11 +874,11 @@ final class LocalBookmarkStore: BookmarkStore { } // 4. Add existing top level entities as children of the new top level folder: - + topLevelFolder.mutableChildren.addObjects(from: existingTopLevelEntities) - + // 5. Save the migration: - + try context.save() } catch { Pixel.fire(.debug(event: .bookmarksStoreRootFolderMigrationFailed, error: error)) @@ -934,19 +934,19 @@ final class LocalBookmarkStore: BookmarkStore { context.performAndWait { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: BookmarkManagedObject.className()) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - + do { try context.execute(deleteRequest) } catch { assertionFailure("Failed to reset bookmarks") } } - + sharedInitialization() } - + // MARK: - Concurrency - + func loadAll(type: BookmarkStoreFetchPredicateType) async -> Result<[BaseBookmarkEntity], Error> { return await withCheckedContinuation { continuation in loadAll(type: type) { result, error in @@ -963,7 +963,7 @@ final class LocalBookmarkStore: BookmarkStore { } } } - + func save(folder: BookmarkFolder, parent: BookmarkFolder?) async -> Result { return await withCheckedContinuation { continuation in save(folder: folder, parent: parent) { result, error in @@ -976,7 +976,7 @@ final class LocalBookmarkStore: BookmarkStore { } } } - + func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?) async -> Result { return await withCheckedContinuation { continuation in save(bookmark: bookmark, parent: parent, index: index) { result, error in @@ -989,7 +989,7 @@ final class LocalBookmarkStore: BookmarkStore { } } } - + func move(objectUUIDs: [UUID], toIndex index: Int?, withinParentFolder parent: ParentFolderType) async -> Error? { return await withCheckedContinuation { continuation in move(objectUUIDs: objectUUIDs, toIndex: index, withinParentFolder: parent) { error in @@ -1048,7 +1048,7 @@ fileprivate extension BookmarkManagedObject { } extension UUID { - + static var rootBookmarkFolderUUID: UUID { return UUID(uuidString: LocalBookmarkStore.Constants.rootFolderUUID)! } @@ -1056,16 +1056,16 @@ extension UUID { static var favoritesFolderUUID: UUID { return UUID(uuidString: LocalBookmarkStore.Constants.favoritesFolderUUID)! } - + } fileprivate extension BookmarkManagedObject { - + var isInvalid: Bool { if titleEncrypted == nil { return true } - + return false } diff --git a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift index d99baf026a..49f694be2b 100644 --- a/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift +++ b/DuckDuckGo/Bookmarks/Services/ContextualMenu.swift @@ -25,7 +25,7 @@ struct ContextualMenu { guard let objects = objects, objects.count > 0 else { return menuForNoSelection() } - + if objects.count > 1, let entities = objects as? [BaseBookmarkEntity] { return menu(for: entities) } @@ -41,7 +41,7 @@ struct ContextualMenu { return nil } } - + // MARK: - Single Item Menu Creation private static func menuForNoSelection() -> NSMenu { @@ -81,7 +81,7 @@ struct ContextualMenu { menu.addItem(renameFolderMenuItem(folder: folder)) menu.addItem(deleteFolderMenuItem(folder: folder)) menu.addItem(NSMenuItem.separator()) - + menu.addItem(openInNewTabsMenuItem(folder: folder)) return menu @@ -100,11 +100,11 @@ struct ContextualMenu { static func deleteFolderMenuItem(folder: BookmarkFolder) -> NSMenuItem { return menuItem(UserText.deleteFolder, #selector(FolderMenuItemSelectors.deleteFolder(_:)), folder) } - + static func openInNewTabsMenuItem(folder: BookmarkFolder) -> NSMenuItem { return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), folder) } - + static func openBookmarksInNewTabsMenuItem(bookmarks: [Bookmark]) -> NSMenuItem { return menuItem(UserText.bookmarksOpenInNewTabs, #selector(FolderMenuItemSelectors.openInNewTabs(_:)), bookmarks) } @@ -128,7 +128,7 @@ struct ContextualMenu { return menuItem(title, #selector(BookmarkMenuItemSelectors.toggleBookmarkAsFavorite(_:)), bookmark) } - + static func addBookmarksToFavoritesMenuItem(bookmarks: [Bookmark], allFavorites: Bool) -> NSMenuItem { let title: String @@ -161,18 +161,18 @@ struct ContextualMenu { item.representedObject = representedObject return item } - + // MARK: - Multi-Item Menu Creation private static func menu(for entities: [BaseBookmarkEntity]) -> NSMenu { let menu = NSMenu(title: "") var menuItems: [NSMenuItem] = [] - + let bookmarks = entities.compactMap({ $0 as? Bookmark }) if !bookmarks.isEmpty { menuItems.append(openBookmarksInNewTabsMenuItem(bookmarks: bookmarks)) - + // If all selected items are bookmarks and they all have the same favourite status, show a menu item to add/remove them all as favourites. if bookmarks.count == entities.count { if bookmarks.allSatisfy({ $0.isFavorite }) { @@ -181,7 +181,7 @@ struct ContextualMenu { menuItems.append(addBookmarksToFavoritesMenuItem(bookmarks: bookmarks, allFavorites: false)) } } - + menuItems.append(NSMenuItem.separator()) } @@ -191,8 +191,8 @@ struct ContextualMenu { menuItems.append(deleteItem) menu.items = menuItems - + return menu } - + } diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkModalViewController.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkModalViewController.swift index 2fbbb4a77d..6c9fc22d23 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkModalViewController.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkModalViewController.swift @@ -78,7 +78,7 @@ final class AddBookmarkModalViewController: NSViewController { } weak var delegate: AddBookmarkModalViewControllerDelegate? - + private var originalBookmark: Bookmark? override func viewDidLoad() { @@ -92,7 +92,7 @@ final class AddBookmarkModalViewController: NSViewController { super.viewWillAppear() applyModalWindowStyleIfNeeded() } - + func edit(bookmark: Bookmark) { self.originalBookmark = bookmark } @@ -105,7 +105,7 @@ final class AddBookmarkModalViewController: NSViewController { guard let url = urlTextField.stringValue.url else { return } - + if let bookmark = originalBookmark { bookmark.title = bookmarkTitleTextField.stringValue delegate?.addBookmarkViewController(self, saved: bookmark, newURL: url) @@ -128,13 +128,13 @@ final class AddBookmarkModalViewController: NSViewController { updateAddButton() } - + private func updateWithExistingBookmark() { if let originalBookmark = originalBookmark { titleTextField.stringValue = UserText.updateBookmark bookmarkTitleTextField.stringValue = originalBookmark.title urlTextField.stringValue = originalBookmark.url.absoluteString - + addButton.title = UserText.save } } diff --git a/DuckDuckGo/Bookmarks/View/AddFolderModalViewController.swift b/DuckDuckGo/Bookmarks/View/AddFolderModalViewController.swift index 0c3d493f90..5dc9cda43b 100644 --- a/DuckDuckGo/Bookmarks/View/AddFolderModalViewController.swift +++ b/DuckDuckGo/Bookmarks/View/AddFolderModalViewController.swift @@ -20,10 +20,10 @@ import AppKit import Combine protocol AddFolderModalViewControllerDelegate: AnyObject { - + func addFolderViewController(_ viewController: AddFolderModalViewController, addedFolderWith name: String) func addFolderViewController(_ viewController: AddFolderModalViewController, saved folder: BookmarkFolder) - + } final class AddFolderModalViewController: NSViewController { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index b56c249db2..e88d7489fc 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -26,12 +26,12 @@ protocol BookmarkListViewControllerDelegate: AnyObject { } final class BookmarkListViewController: NSViewController { - + private enum Constants { static let storyboardName = "Bookmarks" static let identifier = "BookmarkListViewController" } - + static func create() -> BookmarkListViewController { let storyboard = NSStoryboard(name: Constants.storyboardName, bundle: nil) return storyboard.instantiateController(identifier: Constants.identifier) @@ -45,7 +45,7 @@ final class BookmarkListViewController: NSViewController { @IBOutlet var emptyState: NSView! @IBOutlet var emptyStateTitle: NSTextField! @IBOutlet var emptyStateMessage: NSTextField! - + @IBOutlet var newBookmarkButton: NSButton! @IBOutlet var newFolderButton: NSButton! @IBOutlet var manageBookmarksButton: NSButton! @@ -53,39 +53,39 @@ final class BookmarkListViewController: NSViewController { private var cancellables = Set() private var bookmarkManager: BookmarkManager = LocalBookmarkManager.shared private let treeControllerDataSource = BookmarkListTreeControllerDataSource() - + private var mouseUpEventsMonitor: Any? private var mouseDownEventsMonitor: Any? private var appObserver: Any? - + private lazy var treeController: BookmarkTreeController = { return BookmarkTreeController(dataSource: treeControllerDataSource) }() - + private lazy var dataSource: BookmarkOutlineViewDataSource = { BookmarkOutlineViewDataSource(contentMode: .bookmarksAndFolders, treeController: treeController) }() - + private var selectedNodes: [BookmarkNode] { if let nodes = outlineView.selectedItems as? [BookmarkNode] { return nodes } return [BookmarkNode]() } - + override func viewDidLoad() { super.viewDidLoad() view.subscribeForAppApperanceUpdates()?.store(in: &cancellables) preferredContentSize = CGSize(width: 420, height: 500) - + outlineView.register(BookmarkOutlineViewCell.nib, forIdentifier: BookmarkOutlineViewCell.identifier) outlineView.dataSource = dataSource outlineView.delegate = dataSource outlineView.setDraggingSourceOperationMask([.move], forLocal: true) outlineView.registerForDraggedTypes([BookmarkPasteboardWriter.bookmarkUTIInternalType, FolderPasteboardWriter.folderUTIInternalType]) - + LocalBookmarkManager.shared.listPublisher.receive(on: DispatchQueue.main).sink { [weak self] list in self?.reloadData() let isEmpty = list?.topLevelEntities.isEmpty ?? true @@ -95,7 +95,7 @@ final class BookmarkListViewController: NSViewController { emptyStateTitle.attributedStringValue = NSAttributedString.make(emptyStateTitle.stringValue, lineHeight: 1.14, kern: -0.23) emptyStateMessage.attributedStringValue = NSAttributedString.make(emptyStateMessage.stringValue, lineHeight: 1.05, kern: -0.08) - + newBookmarkButton.toolTip = UserText.newBookmarkTooltip newFolderButton.toolTip = UserText.newFolderTooltip manageBookmarksButton.toolTip = UserText.manageBookmarksTooltip @@ -109,31 +109,31 @@ final class BookmarkListViewController: NSViewController { private func reloadData() { let selectedNodes = self.selectedNodes - + dataSource.reloadData() outlineView.reloadData() - + expandAndRestore(selectedNodes: selectedNodes) } - + @IBAction func newBookmarkButtonClicked(_ sender: AnyObject) { let newBookmarkViewController = AddBookmarkModalViewController.create() newBookmarkViewController.currentTabWebsite = currentTabWebsite newBookmarkViewController.delegate = self presentAsModalWindow(newBookmarkViewController) } - + @IBAction func newFolderButtonClicked(_ sender: AnyObject) { let newFolderViewController = AddFolderModalViewController.create() newFolderViewController.delegate = self presentAsModalWindow(newFolderViewController) } - + @IBAction func openManagementInterface(_ sender: NSButton) { WindowControllersManager.shared.showBookmarksTab() delegate?.popoverShouldClose(self) } - + @IBAction func handleClick(_ sender: NSOutlineView) { guard sender.clickedRow != -1 else { return } @@ -156,7 +156,7 @@ final class BookmarkListViewController: NSViewController { } // MARK: NSOutlineView Configuration - + private func expandAndRestore(selectedNodes: [BookmarkNode]) { treeController.visitNodes { node in if let objectID = (node.representedObject as? BaseBookmarkEntity)?.id { @@ -180,13 +180,13 @@ final class BookmarkListViewController: NSViewController { } } } - + restoreSelection(to: selectedNodes) } - + private func restoreSelection(to nodes: [BookmarkNode]) { guard selectedNodes != nodes else { return } - + var indexes = IndexSet() for node in nodes { // The actual instance of the Bookmark may have changed after reloading, so this is a hack to get the right one. @@ -196,185 +196,185 @@ final class BookmarkListViewController: NSViewController { indexes.insert(row) } } - + if indexes.isEmpty { let node = treeController.node(representing: PseudoFolder.bookmarks) let row = outlineView.row(forItem: node as Any) indexes.insert(row) } - + outlineView.selectRowIndexes(indexes, byExtendingSelection: false) } - + } // MARK: - Modal Delegates extension BookmarkListViewController: AddBookmarkModalViewControllerDelegate, AddFolderModalViewControllerDelegate { - + func addBookmarkViewController(_ viewController: AddBookmarkModalViewController, addedBookmarkWithTitle title: String, url: URL) { if !bookmarkManager.isUrlBookmarked(url: url) { bookmarkManager.makeBookmark(for: url, title: title, isFavorite: false) } } - + func addBookmarkViewController(_ viewController: AddBookmarkModalViewController, saved bookmark: Bookmark, newURL: URL) { bookmarkManager.update(bookmark: bookmark) _ = bookmarkManager.updateUrl(of: bookmark, to: newURL) } - + func addFolderViewController(_ viewController: AddFolderModalViewController, addedFolderWith name: String) { bookmarkManager.makeFolder(for: name, parent: nil) } - + func addFolderViewController(_ viewController: AddFolderModalViewController, saved folder: BookmarkFolder) { bookmarkManager.update(folder: folder) } - + } // MARK: - Menu Item Selectors extension BookmarkListViewController: NSMenuDelegate { - + func contextualMenuForClickedRows() -> NSMenu? { let row = outlineView.clickedRow - + guard row != -1 else { return ContextualMenu.menu(for: nil) } - + if outlineView.selectedRowIndexes.contains(row) { return ContextualMenu.menu(for: outlineView.selectedItems, includeBookmarkEditMenu: false) } - + if let item = outlineView.item(atRow: row) { return ContextualMenu.menu(for: [item], includeBookmarkEditMenu: false) } else { return nil } } - + public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - + guard let contextualMenu = contextualMenuForClickedRows() else { return } - + let items = contextualMenu.items contextualMenu.removeAllItems() for menuItem in items { menu.addItem(menuItem) } } - + } extension BookmarkListViewController: BookmarkMenuItemSelectors { - + func openBookmarkInNewTab(_ sender: NSMenuItem) { guard let bookmark = sender.representedObject as? Bookmark else { assertionFailure("Failed to cast menu represented object to Bookmark") return } - + WindowControllersManager.shared.show(url: bookmark.url, newTab: true) } - + func openBookmarkInNewWindow(_ sender: NSMenuItem) { guard let bookmark = sender.representedObject as? Bookmark else { assertionFailure("Failed to cast menu represented object to Bookmark") return } - + WindowsManager.openNewWindow(with: bookmark.url) } - + func toggleBookmarkAsFavorite(_ sender: NSMenuItem) { guard let bookmark = sender.representedObject as? Bookmark else { assertionFailure("Failed to cast menu represented object to Bookmark") return } - + bookmark.isFavorite.toggle() LocalBookmarkManager.shared.update(bookmark: bookmark) } - + func editBookmark(_ sender: NSMenuItem) { // Unsupported in the list view for the initial release. } - + func copyBookmark(_ sender: NSMenuItem) { guard let bookmark = sender.representedObject as? Bookmark, let bookmarkURL = bookmark.url as NSURL? else { assertionFailure("Failed to cast menu represented object to Bookmark") return } - + let pasteboard = NSPasteboard.general pasteboard.declareTypes([.URL], owner: nil) bookmarkURL.write(to: pasteboard) pasteboard.setString(bookmarkURL.absoluteString ?? "", forType: .string) } - + func deleteBookmark(_ sender: NSMenuItem) { guard let bookmark = sender.representedObject as? Bookmark else { assertionFailure("Failed to cast menu represented object to Bookmark") return } - + LocalBookmarkManager.shared.remove(bookmark: bookmark) } - + func deleteEntities(_ sender: NSMenuItem) { guard let uuids = sender.representedObject as? [UUID] else { assertionFailure("Failed to cast menu item's represented object to UUID array") return } - + LocalBookmarkManager.shared.remove(objectsWithUUIDs: uuids) } - + } extension BookmarkListViewController: FolderMenuItemSelectors { - + func newFolder(_ sender: NSMenuItem) { newFolderButtonClicked(sender) } - + func renameFolder(_ sender: NSMenuItem) { guard let folder = sender.representedObject as? BookmarkFolder else { assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") return } - + let addFolderViewController = AddFolderModalViewController.create() addFolderViewController.delegate = self addFolderViewController.edit(folder: folder) presentAsModalWindow(addFolderViewController) } - + func deleteFolder(_ sender: NSMenuItem) { guard let folder = sender.representedObject as? BookmarkFolder else { assertionFailure("Failed to retrieve Bookmark from Delete Folder context menu item") return } - + LocalBookmarkManager.shared.remove(folder: folder) } - + func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, let children = (sender.representedObject as? BookmarkFolder)?.children else { assertionFailure("Cannot open in new tabs") return } - + let tabs = children.compactMap { $0 as? Bookmark }.map { Tab(content: .url($0.url), shouldLoadInBackground: true) } tabCollection.append(tabs: tabs) } - + } // MARK: - BookmarkListPopover diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift index 334734eea3..a0da8b0564 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementDetailViewController.swift @@ -106,7 +106,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem // Clicking anywhere outside of the table view should end editing mode for a given cell. updateEditingState(forRowAt: -1) } - + override func keyDown(with event: NSEvent) { if event.charactersIgnoringModifiers == String(UnicodeScalar(NSDeleteCharacter)!) { deleteSelectedItems() @@ -119,7 +119,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem return } emptyState.isHidden = !(bookmarkManager.list?.topLevelEntities.isEmpty ?? true) - + let scrollPosition = tableView.visibleRect.origin tableView.reloadData() tableView.scroll(scrollPosition) @@ -134,7 +134,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem let entities = sender.selectedRowIndexes.map { fetchEntity(at: $0) } let bookmarks = entities.compactMap { $0 as? Bookmark } openBookmarksInNewTabs(bookmarks) - + return } @@ -177,16 +177,16 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem addFolderViewController.delegate = self beginSheet(addFolderViewController) } - + @IBAction func delete(_ sender: AnyObject) { deleteSelectedItems() } - + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { if menuItem.action == #selector(BookmarkManagementDetailViewController.delete(_:)) { return !tableView.selectedRowIndexes.isEmpty } - + return true } @@ -247,11 +247,11 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem return LocalBookmarkManager.shared.list?.favoriteBookmarks.count ?? 0 } } - + private func deleteSelectedItems() { let entities = tableView.selectedRowIndexes.compactMap { fetchEntity(at: $0) } let entityUUIDs = entities.map(\.id) - + bookmarkManager.remove(objectsWithUUIDs: entityUUIDs) } @@ -260,7 +260,7 @@ final class BookmarkManagementDetailViewController: NSViewController, NSMenuItem // MARK: - Modal Delegates extension BookmarkManagementDetailViewController: AddBookmarkModalViewControllerDelegate, AddFolderModalViewControllerDelegate { - + func addBookmarkViewController(_ viewController: AddBookmarkModalViewController, addedBookmarkWithTitle title: String, url: URL) { guard !bookmarkManager.isUrlBookmarked(url: url) else { return @@ -272,7 +272,7 @@ extension BookmarkManagementDetailViewController: AddBookmarkModalViewController bookmarkManager.makeBookmark(for: url, title: title, isFavorite: false) } } - + func addBookmarkViewController(_ viewController: AddBookmarkModalViewController, saved bookmark: Bookmark, newURL: URL) { bookmarkManager.update(bookmark: bookmark) _ = bookmarkManager.updateUrl(of: bookmark, to: newURL) @@ -357,7 +357,7 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi if let folders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard) { return validateDrop(for: folders, destination: proposedDestination) } - + return .none } else { if dropOperation == .above { @@ -380,7 +380,7 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi guard let destinationFolder = destination as? BookmarkFolder else { return .none } - + for folderID in draggedFolders.map(\.id) { guard let folderUUID = UUID(uuidString: folderID) else { assertionFailure("Failed to convert UUID string to UUID") @@ -471,7 +471,7 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi cell?.isSelected = false } } - + func tableViewSelectionDidChange(_ notification: Notification) { onSelectionChanged() } @@ -484,13 +484,13 @@ extension BookmarkManagementDetailViewController: NSTableViewDelegate, NSTableVi cell?.isSelected = true } } - + fileprivate func openBookmarksInNewTabs(_ bookmarks: [Bookmark]) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel else { assertionFailure("Cannot open in new tabs") return } - + let tabs = bookmarks.map { Tab(content: .url($0.url), shouldLoadInBackground: true) } tabCollection.append(tabs: tabs) } @@ -618,7 +618,7 @@ extension BookmarkManagementDetailViewController: FolderMenuItemSelectors { LocalBookmarkManager.shared.remove(folder: folder) } - + func openInNewTabs(_ sender: NSMenuItem) { if let children = (sender.representedObject as? BookmarkFolder)?.children { let bookmarks = children.compactMap { $0 as? Bookmark } @@ -695,10 +695,10 @@ extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { LocalBookmarkManager.shared.remove(bookmark: bookmark) } - + func deleteEntities(_ sender: NSMenuItem) { let uuids: [UUID] - + if let array = sender.representedObject as? [UUID] { uuids = array } else if let objects = sender.representedObject as? [BaseBookmarkEntity] { @@ -707,8 +707,8 @@ extension BookmarkManagementDetailViewController: BookmarkMenuItemSelectors { assertionFailure("Failed to cast menu item's represented object to UUID array") return } - + LocalBookmarkManager.shared.remove(objectsWithUUIDs: uuids) } - + } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift index f267721ccc..74f5e851e6 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkManagementSidebarViewController.swift @@ -32,7 +32,7 @@ final class BookmarkManagementSidebarViewController: NSViewController { case empty case folder(BookmarkFolder) case favorites - + var selectedFolderUUID: UUID? { switch self { case .folder(let folder): return folder.id @@ -226,7 +226,7 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { assertionFailure("Failed to retrieve Bookmark from Rename Folder context menu item") return } - + let addFolderViewController = AddFolderModalViewController.create() addFolderViewController.delegate = self addFolderViewController.edit(folder: folder) @@ -241,14 +241,14 @@ extension BookmarkManagementSidebarViewController: FolderMenuItemSelectors { LocalBookmarkManager.shared.remove(folder: folder) } - + func openInNewTabs(_ sender: NSMenuItem) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel, let children = (sender.representedObject as? BookmarkFolder)?.children else { assertionFailure("Cannot open in new tabs") return } - + let tabs = children.compactMap { $0 as? Bookmark }.map { Tab(content: .url($0.url), shouldLoadInBackground: true) } tabCollection.append(tabs: tabs) } diff --git a/DuckDuckGo/Bookmarks/View/BookmarkPopoverViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkPopoverViewController.swift index aff0e1d291..755cb29cd2 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkPopoverViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkPopoverViewController.swift @@ -35,7 +35,7 @@ final class BookmarkPopoverViewController: NSViewController { @IBOutlet weak var textField: NSTextField! @IBOutlet weak var favoriteButton: NSButton! @IBOutlet weak var folderPickerPopUpButton: NSPopUpButton! - + private var folderPickerSelectionCancellable: AnyCancellable? let bookmarkManager: BookmarkManager = LocalBookmarkManager.shared @@ -54,12 +54,12 @@ final class BookmarkPopoverViewController: NSViewController { appearanceCancellable = view.subscribeForAppApperanceUpdates() textField.delegate = self - + folderPickerSelectionCancellable = folderPickerPopUpButton.selectionPublisher.dropFirst().sink { [weak self] index in guard let self = self, let bookmark = self.bookmark, let menuItem = self.folderPickerPopUpButton.item(at: index) else { return } - + let folder = menuItem.representedObject as? BookmarkFolder self.bookmarkManager.add(bookmark: bookmark, to: folder, completion: { _ in }) } @@ -75,14 +75,14 @@ final class BookmarkPopoverViewController: NSViewController { @IBAction func removeButtonAction(_ sender: NSButton) { guard let bookmark = bookmark else { return } bookmarkManager.remove(bookmark: bookmark) - + delegate?.popoverShouldClose(self) } @IBAction func doneButtonAction(_ sender: NSButton) { delegate?.popoverShouldClose(self) } - + @IBAction func favoritesButtonAction(_ sender: Any) { guard let bookmark = bookmark else { return } bookmark.isFavorite = !bookmark.isFavorite @@ -106,7 +106,7 @@ final class BookmarkPopoverViewController: NSViewController { favoriteButton.image = bookmark.isFavorite ? Self.favoriteFilledImage : Self.favoriteImage favoriteButton.title = " \(bookmark.isFavorite ? UserText.removeFromFavorites : UserText.addToFavorites)" } - + private func refreshFolderPicker() { guard let list = bookmarkManager.list else { assertionFailure("Tried to refresh bookmark folder picker, but couldn't get bookmark list") @@ -118,42 +118,42 @@ final class BookmarkPopoverViewController: NSViewController { let topLevelFolders = list.topLevelEntities.compactMap { $0 as? BookmarkFolder } var folderMenuItems = [NSMenuItem]() - + folderMenuItems.append(bookmarksMenuItem) folderMenuItems.append(.separator()) folderMenuItems.append(contentsOf: createMenuItems(for: topLevelFolders)) - + folderPickerPopUpButton.menu?.items = folderMenuItems - + let selectedFolderMenuItem = folderMenuItems.first(where: { menuItem in guard let folder = menuItem.representedObject as? BookmarkFolder else { return false } - + return folder.id == bookmark?.parentFolderUUID }) - + folderPickerPopUpButton.select(selectedFolderMenuItem ?? bookmarksMenuItem) } - + private func createMenuItems(for bookmarkFolders: [BookmarkFolder], level: Int = 0) -> [NSMenuItem] { let viewModels = bookmarkFolders.map(BookmarkViewModel.init(entity:)) var menuItems = [NSMenuItem]() - + for viewModel in viewModels { let menuItem = NSMenuItem(bookmarkViewModel: viewModel) menuItem.indentationLevel = level menuItems.append(menuItem) - + if let folder = viewModel.entity as? BookmarkFolder, !folder.children.isEmpty { let childFolders = folder.children.compactMap { $0 as? BookmarkFolder } menuItems.append(contentsOf: createMenuItems(for: childFolders, level: level + 1)) } } - + return menuItems } - + } extension BookmarkPopoverViewController: NSTextFieldDelegate { diff --git a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift index cc4cf271bf..95a337c229 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkTableCellView.swift @@ -138,12 +138,12 @@ final class BookmarkTableCellView: NSTableCellView, NibLoadable { updateTitleLabelValue() } } - + override var draggingImageComponents: [NSDraggingImageComponent] { let faviconComponent = NSDraggingImageComponent(key: .icon) faviconComponent.contents = faviconImageView.image faviconComponent.frame = faviconImageView.frame - + let labelComponent = NSDraggingImageComponent(key: .label) labelComponent.contents = titleLabel.imageRepresentation() labelComponent.frame = titleLabel.frame @@ -182,11 +182,11 @@ final class BookmarkTableCellView: NSTableCellView, NibLoadable { self.entity = bookmark faviconImageView.image = bookmark.favicon(.small) ?? NSImage(named: "BookmarkDefaultFavicon") - + if bookmark.isFavorite { accessoryImageView.isHidden = false } - + 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 0d2dc417ee..309a7d25c9 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift @@ -19,7 +19,7 @@ import AppKit struct BookmarkViewModel { - + let entity: BaseBookmarkEntity var menuTitle: String { @@ -45,7 +45,7 @@ struct BookmarkViewModel { if let bookmark = entity as? Bookmark { let favicon = bookmark.favicon(.small)?.copy() as? NSImage favicon?.size = NSSize.faviconSize - + return favicon ?? NSImage(named: "BookmarkDefaultFavicon") } else if entity is BookmarkFolder { return NSImage(named: "Folder") @@ -70,7 +70,7 @@ struct BookmarkViewModel { guard let bookmark = entity as? Bookmark else { preconditionFailure("\(#file): Attempted to provide representing color for non-Bookmark") } - + let index = bookmark.url.absoluteString.count % Self.representingColors.count return Self.representingColors[index] } diff --git a/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift index d24a8c2306..00dcf8a46d 100644 --- a/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift +++ b/DuckDuckGo/Browser Tab/Extensions/HoveredLinkTabExtension.swift @@ -44,7 +44,7 @@ protocol HoveredLinksProtocol { extension HoveredLinkTabExtension: HoveredLinksProtocol { func getPublicProtocol() -> HoveredLinksProtocol { self } - + var hoveredLinkPublisher: AnyPublisher { hoveredLinkSubject.eraseToAnyPublisher() } diff --git a/DuckDuckGo/Browser Tab/Model/ContentScopeFeatureFlagging.swift b/DuckDuckGo/Browser Tab/Model/ContentScopeFeatureFlagging.swift index cc397260ec..1718eb9ed8 100644 --- a/DuckDuckGo/Browser Tab/Model/ContentScopeFeatureFlagging.swift +++ b/DuckDuckGo/Browser Tab/Model/ContentScopeFeatureFlagging.swift @@ -20,7 +20,7 @@ import Foundation import BrowserServicesKit extension ContentScopeFeatureToggles { - + static let supportedFeaturesOnMacOS = ContentScopeFeatureToggles(emailProtection: true, credentialsAutofill: true, identitiesAutofill: true, diff --git a/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift index 458ef6fad2..8c369fc731 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab+UIDelegate.swift @@ -74,7 +74,7 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate { if let newWindowPolicy = self.contextMenuManager?.decideNewWindowPolicy(for: navigationAction) { return newWindowPolicy } - + return nil }() switch newWindowPolicy { diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index 82aee7460f..533cb7ea55 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -152,7 +152,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { let historyCoordinating: HistoryCoordinating } - // "protected" delegate property for extensions usage + // "protected" delegate property for extensions usage private weak var delegate: TabDelegate? @objc private var objcDelegate: Any? { delegate } static var objcDelegateKeyPath: String { #keyPath(objcDelegate) } @@ -401,7 +401,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { if let title = content.title { self.title = title } - + } } @@ -427,7 +427,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { } } } - + var lastSelectedAt: Date? @Published var title: String? @@ -474,7 +474,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { self.sessionStateData = (try? webView.sessionStateData()) return self.sessionStateData } - + @available(macOS 12, *) func getActualInteractionStateData() -> Data? { if let interactionStateData = interactionStateData { @@ -482,9 +482,9 @@ final class Tab: NSObject, Identifiable, ObservableObject { } guard webView.url != nil else { return nil } - + self.interactionStateData = (webView.interactionState as? Data) - + return self.interactionStateData } @@ -671,7 +671,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { }() if shouldLoadURL(url, shouldLoadInBackground: shouldLoadInBackground) { let didRestore: Bool - + if #available(macOS 12.0, *) { didRestore = restoreInteractionStateDataIfNeeded() || restoreSessionStateDataIfNeeded() } else { @@ -750,10 +750,10 @@ final class Tab: NSObject, Identifiable, ObservableObject { os_log("Tab:setupWebView could not restore session state %s", "\(error)") } } - + return didRestore } - + @MainActor @available(macOS 12, *) private func restoreInteractionStateDataIfNeeded() -> Bool { @@ -762,11 +762,11 @@ final class Tab: NSObject, Identifiable, ObservableObject { if contentURL.isFileURL { _ = webView.loadFileURL(contentURL, allowingReadAccessTo: URL(fileURLWithPath: "/")) } - + webView.interactionState = interactionStateData didRestore = true } - + return didRestore } @@ -871,7 +871,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { } // MARK: - Youtube Player - + private weak var youtubeOverlayScript: YoutubeOverlayUserScript? private weak var youtubePlayerScript: YoutubePlayerUserScript? private var youtubePlayerCancellables: Set = [] @@ -926,7 +926,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { youtubePlayerScript?.isEnabled = false } } - + // MARK: - Dashboard Info @Published private(set) var privacyInfo: PrivacyInfo? private var previousPrivacyInfosByURL: [String: PrivacyInfo] = [:] @@ -944,21 +944,21 @@ final class Tab: NSObject, Identifiable, ObservableObject { privacyInfo = nil } } - + private func makePrivacyInfo(url: URL) -> PrivacyInfo? { guard let host = url.host else { return nil } - + let entity = contentBlocking.trackerDataManager.trackerData.findEntity(forHost: host) - + privacyInfo = PrivacyInfo(url: url, parentEntity: entity, protectionStatus: makeProtectionStatus(for: host)) - + previousPrivacyInfosByURL[url.absoluteString] = privacyInfo - + return privacyInfo } - + private func resetConnectionUpgradedTo(navigationAction: WKNavigationAction) { let isOnUpgradedPage = navigationAction.request.url == privacyInfo?.connectionUpgradedTo if !navigationAction.isTargetingMainFrame || isOnUpgradedPage { return } @@ -974,19 +974,19 @@ final class Tab: NSObject, Identifiable, ObservableObject { if upgradedUrl == nil { return } privacyInfo?.connectionUpgradedTo = upgradedUrl } - + private func makeProtectionStatus(for host: String) -> ProtectionStatus { let config = contentBlocking.privacyConfigurationManager.privacyConfig - + let isTempUnprotected = config.isTempUnprotected(domain: host) let isAllowlisted = config.isUserUnprotected(domain: host) - + var enabledFeatures: [String] = [] - + if !config.isInExceptionList(domain: host, forFeature: .contentBlocking) { enabledFeatures.append(PrivacyFeature.contentBlocking.rawValue) } - + return ProtectionStatus(unprotectedTemporary: isTempUnprotected, enabledFeatures: enabledFeatures, allowlisted: isAllowlisted, @@ -1050,10 +1050,10 @@ extension Tab: ContentBlockerRulesUserScriptDelegate { func contentBlockerRulesUserScriptShouldProcessCTLTrackers(_ script: ContentBlockerRulesUserScript) -> Bool { return fbBlockingEnabled } - + func contentBlockerRulesUserScript(_ script: ContentBlockerRulesUserScript, detectedTracker tracker: DetectedRequest) { guard let url = webView.url else { return } - + privacyInfo?.trackerInfo.addDetectedTracker(tracker, onPageWithURL: url) historyCoordinating.addDetectedTracker(tracker, onURL: url) } @@ -1061,7 +1061,7 @@ extension Tab: ContentBlockerRulesUserScriptDelegate { func contentBlockerRulesUserScript(_ script: ContentBlockerRulesUserScript, detectedThirdPartyRequest request: DetectedRequest) { privacyInfo?.trackerInfo.add(detectedThirdPartyRequest: request) } - + } extension HistoryCoordinating { @@ -1100,10 +1100,10 @@ extension Tab: SurrogatesUserScriptDelegate { func surrogatesUserScript(_ script: SurrogatesUserScript, detectedTracker tracker: DetectedRequest, withSurrogate host: String) { guard let url = webView.url else { return } - + privacyInfo?.trackerInfo.addInstalledSurrogateHost(host, for: tracker, onPageWithURL: url) privacyInfo?.trackerInfo.addDetectedTracker(tracker, onPageWithURL: url) - + historyCoordinating.addDetectedTracker(tracker, onURL: url) } } @@ -1114,7 +1114,7 @@ extension Tab: WKNavigationDelegate { didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { webViewDidReceiveChallengePublisher.send() - + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic { let dialog = UserDialogType.basicAuthenticationChallenge(.init(challenge.protectionSpace) { result in let (disposition, credential) = (try? result.get()) ?? (nil, nil) @@ -1182,7 +1182,7 @@ extension Tab: WKNavigationDelegate { let shouldSelectNewTab = NSApp.isShiftPressed || (isNavigatingAwayFromPinnedTab && !isMiddleButtonClicked && !NSApp.isCommandPressed) didGoBackForward = (navigationAction.navigationType == .backForward) - + // This check needs to happen before GPC checks. Otherwise the navigation type may be rewritten to `.other` // which would skip link rewrites. if navigationAction.navigationType != .backForward { @@ -1235,7 +1235,7 @@ extension Tab: WKNavigationDelegate { self.webView.frozenCanGoForward = nil self.webView.frozenCanGoBack = nil return .cancel - + } else if navigationAction.navigationType != .backForward, !isRequestingNewTab, let request = GPCRequestFactory().requestForGPC(basedOn: navigationAction.request, @@ -1300,12 +1300,12 @@ extension Tab: WKNavigationDelegate { } } } - + if navigationAction.isTargetingMainFrame, navigationAction.request.url?.isDuckDuckGo == true, navigationAction.request.value(forHTTPHeaderField: Constants.ddgClientHeaderKey) == nil, navigationAction.navigationType != .backForward { - + var request = navigationAction.request request.setValue(Constants.ddgClientHeaderValue, forHTTPHeaderField: Constants.ddgClientHeaderKey) _ = webView.load(request) @@ -1405,7 +1405,7 @@ extension Tab: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { userEnteredUrl = false // subsequent requests will be navigations - + let isSuccessfulResponse = (navigationResponse.response as? HTTPURLResponse)?.validateStatusCode(statusCode: 200..<300) == nil if !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload { @@ -1423,11 +1423,11 @@ extension Tab: WKNavigationDelegate { return .download(navigationResponse, using: webView) } } - + if navigationResponse.isForMainFrame && isSuccessfulResponse { self.adClickAttribution?.detection.on2XXResponse(url: webView.url) } - + await self.adClickAttribution?.logic.onProvisionalNavigation() return .allow @@ -1547,7 +1547,7 @@ extension Tab: WKNavigationDelegate { guard frame.isMainFrame else { return } self.mainFrameLoadState = .finished } - + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { Pixel.fire(.debug(event: .webKitDidTerminate)) } @@ -1593,7 +1593,7 @@ extension Tab: AutoconsentUserScriptDelegate { func autoconsentUserScript(consentStatus: CookieConsentInfo) { self.privacyInfo?.cookieConsentManaged = consentStatus } - + func autoconsentUserScriptPromptUserForConsent(_ result: @escaping (Bool) -> Void) { delegate?.tab(self, promptUserForCookieConsent: result) } diff --git a/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift b/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift index 0794326889..9e472f5784 100644 --- a/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift +++ b/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift @@ -83,7 +83,7 @@ extension UserDialogRequest where Info == Void { convenience init(callback: @escaping Callback) { self.init((), callback: callback) } - + } extension UserDialogRequest where Output == Void { diff --git a/DuckDuckGo/Browser Tab/Services/WebsiteDataStore.swift b/DuckDuckGo/Browser Tab/Services/WebsiteDataStore.swift index 5441878c5d..f90fb449c2 100644 --- a/DuckDuckGo/Browser Tab/Services/WebsiteDataStore.swift +++ b/DuckDuckGo/Browser Tab/Services/WebsiteDataStore.swift @@ -50,7 +50,7 @@ internal class WebCacheManager { func clear(domains: Set? = nil) async { // first cleanup ~/Library/Caches await clearFileCache() - + await clearDeviceHashSalts() await removeAllSafelyRemovableDataTypes() @@ -87,14 +87,14 @@ internal class WebCacheManager { Process("/bin/rm", "-rf", tmpDir.path).launch() } - + private func clearDeviceHashSalts() async { guard let bundleID = Bundle.main.bundleIdentifier, var libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else { return } libraryURL.appendPathComponent("WebKit/\(bundleID)/WebsiteData/DeviceIdHashSalts/1") - + let fm = FileManager.default let tmpDir = fm.temporaryDirectory(appropriateFor: libraryURL).appendingPathComponent(UUID().uuidString) @@ -104,7 +104,7 @@ internal class WebCacheManager { os_log("Could not create temporary directory: %s", type: .error, "\(error)") return } - + try? fm.moveItem(at: libraryURL, to: tmpDir.appendingPathComponent("1")) try? fm.createDirectory(at: libraryURL, withIntermediateDirectories: false, diff --git a/DuckDuckGo/Browser Tab/View/WebViewContainerView.swift b/DuckDuckGo/Browser Tab/View/WebViewContainerView.swift index 17dbb4f0a2..b5e282382b 100644 --- a/DuckDuckGo/Browser Tab/View/WebViewContainerView.swift +++ b/DuckDuckGo/Browser Tab/View/WebViewContainerView.swift @@ -32,7 +32,7 @@ final class WebViewContainerView: NSView { init(webView: WebView, frame: NSRect) { self.webView = webView super.init(frame: frame) - + self.autoresizingMask = [.width, .height] webView.translatesAutoresizingMaskIntoConstraints = true @@ -42,7 +42,7 @@ final class WebViewContainerView: NSView { displayedView.autoresizingMask = [.width, .height] self.addSubview(displayedView) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift index 92500d9064..83df3e8481 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift @@ -151,7 +151,7 @@ final class TabViewModel { tab.permissions.$authorizationQuery.assign(to: \.permissionAuthorizationQuery, onWeaklyHeld: self) .store(in: &cancellables) } - + private func subscribeToAppearancePreferences() { appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } @@ -180,11 +180,11 @@ final class TabViewModel { private func updateCanBeBookmarked() { canBeBookmarked = tab.content.url ?? .blankPage != .blankPage } - + private var tabURL: URL? { return tab.content.url ?? tab.parentTab?.content.url } - + private var tabHostURL: URL? { return tabURL?.root } @@ -226,7 +226,7 @@ final class TabViewModel { updatePassiveAddressBarString(showURL: appearancePreferences.showFullURL, url: url, hostURL: hostURL) } - + private func updatePassiveAddressBarString(showURL: Bool, url: URL, hostURL: URL) { if showURL { passiveAddressBarString = url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) diff --git a/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift b/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift index 5c65f605e7..78f095d8c4 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift @@ -24,7 +24,7 @@ final class WebViewStateObserver: NSObject { weak var webView: WKWebView? weak var tabViewModel: TabViewModel? - + private var isObserving = false init(webView: WKWebView, @@ -36,7 +36,7 @@ final class WebViewStateObserver: NSObject { matchFlagValues() observe(webView: webView) } - + func stopObserving() { guard isObserving else { return } webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) @@ -46,7 +46,7 @@ final class WebViewStateObserver: NSObject { webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.serverTrust)) - + isObserving = false } @@ -78,7 +78,7 @@ final class WebViewStateObserver: NSObject { webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: .new, context: nil) webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil) webView.addObserver(self, forKeyPath: #keyPath(WKWebView.serverTrust), options: .new, context: nil) - + isObserving = true } diff --git a/DuckDuckGo/Common/AppVersion.swift b/DuckDuckGo/Common/AppVersion.swift index 358a83da10..56c0a84d84 100644 --- a/DuckDuckGo/Common/AppVersion.swift +++ b/DuckDuckGo/Common/AppVersion.swift @@ -21,7 +21,7 @@ import Foundation struct AppVersion { static let shared = AppVersion() - + private let bundle: Bundle init(bundle: Bundle = .main) { @@ -35,7 +35,7 @@ struct AppVersion { var identifier: String { return bundle.object(forInfoDictionaryKey: Bundle.Keys.identifier) as? String ?? "" } - + var majorVersionNumber: String { return String(versionNumber.split(separator: ".").first ?? "") } @@ -47,5 +47,5 @@ struct AppVersion { var buildNumber: String { return bundle.object(forInfoDictionaryKey: Bundle.Keys.buildNumber) as? String ?? "" } - + } diff --git a/DuckDuckGo/Common/Database/Database.swift b/DuckDuckGo/Common/Database/Database.swift index 1027b1a5f6..7106047f58 100644 --- a/DuckDuckGo/Common/Database/Database.swift +++ b/DuckDuckGo/Common/Database/Database.swift @@ -22,11 +22,11 @@ import BrowserServicesKit import Persistence final class Database { - + fileprivate struct Constants { static let databaseName = "Database" } - + static let shared: CoreDataDatabase = { let (database, error) = makeDatabase() if database == nil { diff --git a/DuckDuckGo/Common/Extensions/ContiguousBytesExtension.swift b/DuckDuckGo/Common/Extensions/ContiguousBytesExtension.swift index eea4f619ff..cf07b71ed9 100644 --- a/DuckDuckGo/Common/Extensions/ContiguousBytesExtension.swift +++ b/DuckDuckGo/Common/Extensions/ContiguousBytesExtension.swift @@ -19,7 +19,7 @@ import Foundation extension ContiguousBytes { - + var dataRepresentation: Data { return self.withUnsafeBytes { bytes in let data = CFDataCreateWithBytesNoCopy(nil, bytes.baseAddress?.assumingMemoryBound(to: UInt8.self), bytes.count, kCFAllocatorNull) diff --git a/DuckDuckGo/Common/Extensions/DateExtension.swift b/DuckDuckGo/Common/Extensions/DateExtension.swift index e70fc8f3c5..77838b7181 100644 --- a/DuckDuckGo/Common/Extensions/DateExtension.swift +++ b/DuckDuckGo/Common/Extensions/DateExtension.swift @@ -24,7 +24,7 @@ extension Date { let name: String let index: Int } - + var components: DateComponents { return Calendar.current.dateComponents([.day, .year, .month], from: self) } diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index 984ca55539..056f1a0f9c 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -112,5 +112,5 @@ extension FileManager { return self.temporaryDirectory } } - + } diff --git a/DuckDuckGo/Common/Extensions/KeyedCodingExtension.swift b/DuckDuckGo/Common/Extensions/KeyedCodingExtension.swift index 3f3aff837d..021a8482ab 100644 --- a/DuckDuckGo/Common/Extensions/KeyedCodingExtension.swift +++ b/DuckDuckGo/Common/Extensions/KeyedCodingExtension.swift @@ -25,7 +25,7 @@ struct JSONCodingKeys: CodingKey { init?(stringValue: String) { self.stringValue = stringValue } - + init?(intValue: Int) { self.init(stringValue: "\(intValue)") self.intValue = intValue @@ -148,7 +148,7 @@ extension KeyedEncodingContainer { } } } - + mutating func encodeIfPresent(_ value: [Any]?, forKey key: KeyedEncodingContainer.Key) throws { guard let safeValue = value else { return @@ -176,7 +176,7 @@ extension UnkeyedEncodingContainer { try self.encodeIfPresent(dict) } } - + mutating func encodeIfPresent(_ value: [String: Any]) throws { var container = self.nestedContainer(keyedBy: JSONCodingKeys.self) for item in value { diff --git a/DuckDuckGo/Common/Extensions/LocaleExtension.swift b/DuckDuckGo/Common/Extensions/LocaleExtension.swift index 14e776f9d9..254f0f87ba 100644 --- a/DuckDuckGo/Common/Extensions/LocaleExtension.swift +++ b/DuckDuckGo/Common/Extensions/LocaleExtension.swift @@ -19,7 +19,7 @@ import Foundation extension Locale { - + enum DateComponentOrder { case dayMonthYear case monthDayYear @@ -30,12 +30,12 @@ extension Locale { // Default to the North American ordering. return .monthDayYear } - + if format.hasPrefix("d") { return .dayMonthYear } else { return .monthDayYear } } - + } diff --git a/DuckDuckGo/Common/Extensions/NSColorExtension.swift b/DuckDuckGo/Common/Extensions/NSColorExtension.swift index 4eaf54e8be..755cb1c5c9 100644 --- a/DuckDuckGo/Common/Extensions/NSColorExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSColorExtension.swift @@ -23,7 +23,7 @@ extension NSColor { static var homePageBackgroundColor: NSColor { NSColor(named: "HomePageBackgroundColor")! } - + static var homePageSearchBarBackgroundColor: NSColor { return NSColor(named: "HomePageSearchBarBackgroundColor")! } @@ -31,7 +31,7 @@ extension NSColor { static var addressBarFocusedBackgroundColor: NSColor { NSColor(named: "AddressBarFocusedBackgroundColor")! } - + static var addressBarBackgroundColor: NSColor { NSColor(named: "AddressBarBackgroundColor")! } @@ -43,7 +43,7 @@ extension NSColor { static var addressBarShadowColor: NSColor { NSColor(named: "AddressBarShadowColor")! } - + static var addressBarSolidSeparatorColor: NSColor { NSColor(named: "AddressBarSolidSeparatorColor")! } @@ -61,15 +61,15 @@ extension NSColor { static var findInPageFocusedBackgroundColor: NSColor { NSColor(named: "FindInPageFocusedBackgroundColor")! } - + static var inactiveSearchBarBackground: NSColor { NSColor(named: "InactiveSearchBarBackground")! } - + static var suggestionTextColor: NSColor { NSColor(named: "SuggestionTextColor")! } - + static var suggestionIconColor: NSColor { NSColor(named: "SuggestionIconColor")! } @@ -81,7 +81,7 @@ extension NSColor { static var interfaceBackgroundColor: NSColor { NSColor(named: "InterfaceBackgroundColor")! } - + static var tabMouseOverColor: NSColor { NSColor(named: "TabMouseOverColor")! } diff --git a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift index 13a3adce51..75f15542e3 100644 --- a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift +++ b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift @@ -42,14 +42,14 @@ extension NSImage { } return self } - + func tinted(with color: NSColor) -> NSImage { guard let image = self.copy() as? NSImage else { return self } image.lockFocus() - + color.set() let imageRect = NSRect(origin: .zero, size: image.size) imageRect.fill(using: .sourceAtop) diff --git a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift index ea099c7175..a7e48b94d1 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift @@ -23,7 +23,7 @@ extension NSMenuItem { static var empty: NSMenuItem { return NSMenuItem(title: UserText.bookmarksBarFolderEmpty, action: nil, target: nil, keyEquivalent: "") } - + convenience init(title string: String, action selector: Selector?, target: AnyObject?, keyEquivalent charCode: String = "", representedObject: Any? = nil) { self.init(title: string, action: selector, keyEquivalent: charCode) self.target = target @@ -34,7 +34,7 @@ extension NSMenuItem { self.init() self.action = selector } - + convenience init(bookmarkViewModel: BookmarkViewModel) { self.init() diff --git a/DuckDuckGo/Common/Extensions/NSPasteboardExtension.swift b/DuckDuckGo/Common/Extensions/NSPasteboardExtension.swift index 4ad6324134..e1851319fe 100644 --- a/DuckDuckGo/Common/Extensions/NSPasteboardExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSPasteboardExtension.swift @@ -24,7 +24,7 @@ extension NSPasteboard { NSPasteboard.general.clearContents() NSPasteboard.general.setString(string, forType: .string) } - + func copy(url: URL) { let url = url as NSURL diff --git a/DuckDuckGo/Common/Extensions/NSPasteboardItemExtension.swift b/DuckDuckGo/Common/Extensions/NSPasteboardItemExtension.swift index 25a143d2a1..ed0933f45f 100644 --- a/DuckDuckGo/Common/Extensions/NSPasteboardItemExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSPasteboardItemExtension.swift @@ -19,18 +19,18 @@ import Foundation extension NSPasteboardItem { - + var bookmarkEntityUUID: UUID? { if let bookmark = propertyList(forType: BookmarkPasteboardWriter.bookmarkUTIInternalType) as? PasteboardAttributes, let bookmarkID = bookmark[PasteboardBookmark.Key.id] { return UUID(uuidString: bookmarkID) } - + if let folder = propertyList(forType: FolderPasteboardWriter.folderUTIInternalType) as? PasteboardAttributes, let folderID = folder[PasteboardFolder.Key.id] { return UUID(uuidString: folderID) } - + return nil } @@ -38,10 +38,10 @@ extension NSPasteboardItem { guard let urlString = string(forType: .URL), let url = URL(string: urlString) else { return nil } - + // WKWebView pasteboard items include the name of the link under the `public.url-name` type. let name = string(forType: NSPasteboard.PasteboardType(rawValue: "public.url-name")) return (title: name, url: url) } - + } diff --git a/DuckDuckGo/Common/Extensions/NSStackViewExtension.swift b/DuckDuckGo/Common/Extensions/NSStackViewExtension.swift index 1ad961d78e..23fb17763b 100644 --- a/DuckDuckGo/Common/Extensions/NSStackViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSStackViewExtension.swift @@ -19,17 +19,17 @@ import AppKit extension NSStackView { - + func addArrangedSubview(_ view: NSView?) { if let view = view { self.addArrangedSubview(view) } } - + func setCustomSpacingAfterLastView(_ spacing: CGFloat) { if let view = arrangedSubviews.last { setCustomSpacing(spacing, after: view) } } - + } diff --git a/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift b/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift index 7769703650..493c63b0d7 100644 --- a/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift @@ -23,28 +23,28 @@ extension NSTextField { static func label(titled title: String) -> NSTextField { let label = NSTextField(string: title) - + label.isEditable = false label.isBordered = false label.isSelectable = false label.isBezeled = false label.backgroundColor = .clear - + return label } - + func setEditable(_ editable: Bool) { self.isEditable = editable self.isBordered = editable self.isSelectable = editable self.isBezeled = editable } - + static func optionalLabel(titled title: String?) -> NSTextField? { guard let title = title else { return nil } - + return label(titled: title) } @@ -78,5 +78,5 @@ extension NSTextField { CATransaction.commit() } - + } diff --git a/DuckDuckGo/Common/Extensions/NSTextViewExtension.swift b/DuckDuckGo/Common/Extensions/NSTextViewExtension.swift index c02cf3cb25..1d1c341ca0 100644 --- a/DuckDuckGo/Common/Extensions/NSTextViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSTextViewExtension.swift @@ -28,11 +28,11 @@ extension NSTextView { return nil }.joined(separator: "\n") } - + func applyLabelStyle() { self.isEditable = false self.backgroundColor = .clear self.textContainer?.textView?.alignment = .center } - + } diff --git a/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift b/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift index 3536703e9b..67baf9d961 100644 --- a/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift @@ -73,7 +73,7 @@ extension NSViewController { removeFromParent() view.removeFromSuperview() } - + func withoutAnimation(_ closure: () -> Void) { CATransaction.begin() CATransaction.setDisableActions(true) diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index d953c2ac41..d99daef2ac 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -131,7 +131,7 @@ extension NSView { return locationInView } } - + func imageRepresentation() -> NSImage { let imageRepresentation = bitmapImageRepForCachingDisplay(in: bounds)! cacheDisplay(in: bounds, to: imageRepresentation) diff --git a/DuckDuckGo/Common/Extensions/String+Punycode.swift b/DuckDuckGo/Common/Extensions/String+Punycode.swift index 326fa47c1a..2201323473 100644 --- a/DuckDuckGo/Common/Extensions/String+Punycode.swift +++ b/DuckDuckGo/Common/Extensions/String+Punycode.swift @@ -27,5 +27,5 @@ extension String { .map { $0.idnaEncoded ?? $0 } .joined(separator: ".") } - + } diff --git a/DuckDuckGo/Common/Extensions/URLRequestExtension.swift b/DuckDuckGo/Common/Extensions/URLRequestExtension.swift index 062111d459..b25a3968b3 100644 --- a/DuckDuckGo/Common/Extensions/URLRequestExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLRequestExtension.swift @@ -33,7 +33,7 @@ extension URLRequest { forHTTPHeaderField: HeaderKey.acceptEncoding.rawValue) let userAgent = UserAgent.duckDuckGoUserAgent() - + request.setValue(userAgent, forHTTPHeaderField: HeaderKey.userAgent.rawValue) let languages = Locale.preferredLanguages.prefix(6) diff --git a/DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift b/DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift index 197d89ae72..abb6a94dbf 100644 --- a/DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift @@ -27,7 +27,7 @@ extension WKUserContentController { add(userScript, name: messageName) } } - + func addHandler(_ userScript: UserScript) { for messageName in userScript.messageNames { if #available(macOS 11.0, *) { diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index 21b54052ea..dc6210b3e4 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -126,7 +126,7 @@ extension WKWebView { } self._stopMediaCapture() } - + func stopAllMediaPlayback() { guard self.responds(to: #selector(_stopAllMediaPlayback)) else { assertionFailure("WKWebView does not respond to _stopAllMediaPlayback") @@ -134,7 +134,7 @@ extension WKWebView { } self._stopAllMediaPlayback() } - + func setPermissions(_ permissions: [PermissionType], muted: Bool) { for permission in permissions { switch permission { @@ -210,7 +210,7 @@ extension WKWebView { return self.instancesRespond(to: #selector(WKWebView._printOperation(with:))) } } - + func printOperation(with printInfo: NSPrintInfo = .shared, for frame: Any?) -> NSPrintOperation? { if let frame = frame, self.responds(to: #selector(WKWebView._printOperation(with:forFrame:))) { @@ -228,7 +228,7 @@ extension WKWebView { printInfo.topMargin = 0 printInfo.bottomMargin = 0 printInfo.scalingFactor = 0.95 - + return self.printOperation(with: printInfo) } diff --git a/DuckDuckGo/Common/Extensions/WKWebsiteDataStoreExtension.swift b/DuckDuckGo/Common/Extensions/WKWebsiteDataStoreExtension.swift index 88f7fcb132..9ea94712a8 100644 --- a/DuckDuckGo/Common/Extensions/WKWebsiteDataStoreExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebsiteDataStoreExtension.swift @@ -19,14 +19,14 @@ import WebKit extension WKWebsiteDataStore { - + /// All website data types except cookies. This set includes those types not publicly declared by WebKit. /// Cookies are not removed as they are handled separately by the Fire button logic. /// /// - note: The full list of data types can be found in the [WKWebsiteDataStore](https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataRecord.mm) documentation. static var allWebsiteDataTypesExceptCookies: Set { var types = Self.allWebsiteDataTypes() - + types.insert("_WKWebsiteDataTypeMediaKeys") types.insert("_WKWebsiteDataTypeHSTSCache") types.insert("_WKWebsiteDataTypeSearchFieldRecentSearches") @@ -37,10 +37,10 @@ extension WKWebsiteDataStore { types.insert("_WKWebsiteDataTypeAlternativeServices") types.remove(WKWebsiteDataTypeCookies) - + return types } - + /// All website data types that are safe to remove from all domains, regardless of their Fireproof status. This set includes those types not publicly declared by WebKit. /// Cookies are not removed as they are handled separately by the Fire button logic. /// @@ -49,14 +49,14 @@ extension WKWebsiteDataStore { var types = Self.allWebsiteDataTypesExceptCookies types.remove(WKWebsiteDataTypeLocalStorage) - + // Only Fireproof IndexedDB on macOS 12.2+. Earlier versions have a privacy flaw that can expose browsing history. // More info: https://fingerprintjs.com/blog/indexeddb-api-browser-vulnerability-safari-15 if #available(macOS 12.2, *) { types.remove(WKWebsiteDataTypeIndexedDBDatabases) } - + return types } - + } diff --git a/DuckDuckGo/Common/File System/EncryptionKeys/EncryptionKeyStore.swift b/DuckDuckGo/Common/File System/EncryptionKeys/EncryptionKeyStore.swift index ec34ed8631..5548cb0d1f 100644 --- a/DuckDuckGo/Common/File System/EncryptionKeys/EncryptionKeyStore.swift +++ b/DuckDuckGo/Common/File System/EncryptionKeys/EncryptionKeyStore.swift @@ -26,7 +26,7 @@ enum EncryptionKeyStoreError: Error { } final class EncryptionKeyStore: EncryptionKeyStoring { - + enum Constants { static let encryptionKeyAccount = "com.duckduckgo.macos.browser" static let encryptionKeyService = "DuckDuckGo Privacy Browser Data Encryption Key" diff --git a/DuckDuckGo/Common/File System/FileStore.swift b/DuckDuckGo/Common/File System/FileStore.swift index aff097b70f..99b03e5a01 100644 --- a/DuckDuckGo/Common/File System/FileStore.swift +++ b/DuckDuckGo/Common/File System/FileStore.swift @@ -78,7 +78,7 @@ final class EncryptedFileStore: FileStore { func hasData(at url: URL) -> Bool { return FileManager.default.fileExists(atPath: url.path) } - + func directoryContents(at path: String) throws -> [String] { return try FileManager.default.contentsOfDirectory(atPath: path) } @@ -115,7 +115,7 @@ extension FileManager: FileStore { func hasData(at url: URL) -> Bool { return fileExists(atPath: url.path) } - + func directoryContents(at path: String) throws -> [String] { return try contentsOfDirectory(atPath: path) } diff --git a/DuckDuckGo/Common/File System/TemporaryFileHandler.swift b/DuckDuckGo/Common/File System/TemporaryFileHandler.swift index 4a96e8e57f..ed04fa81ca 100644 --- a/DuckDuckGo/Common/File System/TemporaryFileHandler.swift +++ b/DuckDuckGo/Common/File System/TemporaryFileHandler.swift @@ -19,60 +19,60 @@ import Foundation final class TemporaryFileHandler { - + enum FileHandlerError: Error { case noFileFound case failedToCopyFile } - + let fileURL: URL let temporaryFileURL: URL - + init(fileURL: URL) { self.fileURL = fileURL - + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let fileExtension = fileURL.pathExtension let newFileName = UUID().uuidString let finalTemporaryFileURL = temporaryDirectoryURL.appendingPathComponent(newFileName).appendingPathExtension(fileExtension) - + self.temporaryFileURL = finalTemporaryFileURL } - + deinit { deleteTemporarilyCopiedFile() } - + func withTemporaryFile(_ closure: (URL) -> T) throws -> T { let temporaryFileURL = try copyFileToTemporaryDirectory() defer { deleteTemporarilyCopiedFile() } return closure(temporaryFileURL) } - + func copyFileToTemporaryDirectory() throws -> URL { let fileManager = FileManager.default - + guard fileManager.fileExists(atPath: fileURL.path) else { throw FileHandlerError.noFileFound } - + do { try fileManager.copyItem(at: fileURL, to: temporaryFileURL) } catch { throw FileHandlerError.failedToCopyFile } - + return temporaryFileURL } - + func deleteTemporarilyCopiedFile() { try? FileManager.default.removeItem(at: temporaryFileURL) } - + } extension URL { - + func withTemporaryFile(_ closure: (URL) -> T) throws -> T { let handler = TemporaryFileHandler(fileURL: self) return try handler.withTemporaryFile(closure) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 3cd8e55ae4..551b86244b 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -98,7 +98,7 @@ struct UserText { static let shareMenuItem = NSLocalizedString("share.menu.item", value: "Share", comment: "Menu item title") static let printMenuItem = NSLocalizedString("print.menu.item", value: "Print…", comment: "Menu item title") static let newWindowMenuItem = NSLocalizedString("new.window.menu.item", value: "New Window", comment: "Menu item title") - + static let fireproofSites = NSLocalizedString("fireproof.sites", value: "Fireproof Sites", comment: "Fireproof sites list title") static let fireproofCheckboxTitle = NSLocalizedString("fireproof.checkbox.title", value: "Ask to Fireproof websites when signing in", comment: "Fireproof settings checkbox title") static let fireproofExplanation = NSLocalizedString("fireproof.explanation", value: "Websites rely on cookies to keep you signed in. When you Fireproof a site, cookies won’t be erased and you'll stay signed in, even after using the Fire Button. We still block third-party trackers found on Fireproof websites.", comment: "Fireproofing mechanism explanation") @@ -152,7 +152,7 @@ struct UserText { static let gpcCheckboxTitle = NSLocalizedString("gpc.checkbox.title", value: "Enable Global Privacy Control", comment: "GPC settings checkbox title") static let gpcExplanation = NSLocalizedString("gpc.explanation", value: "DuckDuckGo automatically blocks many trackers. With Global Privacy Control (GPC), you can also ask participating websites to restrict selling or sharing your personal data with other companies.", comment: "GPC explanation in settings") static let gpcLearnMore = NSLocalizedString("gpc.learnmore.link", value: "Learn More", comment: "Learn More link") - + static let autofillPasswordManager = NSLocalizedString("autofill.password-manager", value: "Password Manager", comment: "Autofill settings section title") static let autofillPasswordManagerDuckDuckGo = NSLocalizedString("autofill.password-manager.duckduckgo", value: "DuckDuckGo built-in password manager", comment: "Autofill password manager row title") static let autofillPasswordManagerBitwarden = NSLocalizedString("autofill.password-manager.bitwarden", value: "Bitwarden", comment: "Autofill password manager row title") @@ -160,7 +160,7 @@ struct UserText { static let restartBitwarden = NSLocalizedString("restart.bitwarden", value: "Restart Bitwarden", comment: "Button to restart Bitwarden application") static let restartBitwardenInfo = NSLocalizedString("restart.bitwarden.info", value: "Bitwarden is not responding. Please restart it to initiate the communication again", comment: "") - + static let autofillAskToSave = NSLocalizedString("autofill.ask-to-save", value: "Ask to Save", comment: "Autofill settings section title") static let autofillAskToSaveExplanation = NSLocalizedString("autofill.ask-to-save.explanation", value: "Receive prompts to save new Autofill information when filling out online forms.", comment: "Description of Autofill autosaving feature - used in settings") static let autofillUsernamesAndPasswords = NSLocalizedString("autofill.usernames-and-passwords", value: "Usernames and passwords", comment: "Autofill autosaved data type") @@ -209,7 +209,7 @@ struct UserText { static let newFolder = NSLocalizedString("folder.optionsMenu.newFolder", value: "New Folder", comment: "Option for creating a new folder") static let renameFolder = NSLocalizedString("folder.optionsMenu.renameFolder", value: "Rename Folder", comment: "Option for renaming a folder") static let deleteFolder = NSLocalizedString("folder.optionsMenu.deleteFolder", value: "Delete Folder", comment: "Option for deleting a folder") - + static let updateBookmark = NSLocalizedString("bookmark.update", value: "Update Bookmark", comment: "Option for updating a bookmark") static let failedToOpenExternally = NSLocalizedString("open.externally.failed", value: "The app required to open that link can’t be found", comment: "’Link’ is link on a website") @@ -276,7 +276,7 @@ struct UserText { static let showFullWebsiteAddress = NSLocalizedString("preferences.appearance.show-full-url", value: "Show full website address", comment: "Option to show full URL in the address bar") static let showAutocompleteSuggestions = NSLocalizedString("preferences.appearance.show-autocomplete-suggestions", value: "Show autocomplete suggestions", comment: "Option to show autocomplete suggestions in the address bar") static let autofill = NSLocalizedString("preferences.autofill", value: "Autofill", comment: "Show Autofill preferences") - + static let aboutDuckDuckGo = NSLocalizedString("preferences.about.about-duckduckgo", value: "About DuckDuckGo", comment: "About screen") static let privacySimplified = NSLocalizedString("preferences.about.privacy-simplified", value: "Privacy, simplified.", comment: "About screen") @@ -289,7 +289,7 @@ struct UserText { static let feedbackBreakageDisclaimer = NSLocalizedString("feedback.breakage.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your selection above, your optional message, the URL, a list of trackers we found on the site, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form") static let feedbackDisclaimer = NSLocalizedString("feedback.disclaimer", value: "Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version.", comment: "Disclaimer in breakage form") - + static let feedbackBugDescription = NSLocalizedString("feedback.bug.description", value: "Please describe the problem in as much detail as possible:", comment: "Label in the feedback form") static let feedbackFeatureRequestDescription = NSLocalizedString("feedback.feature.request.description", value: "What feature would you like to see?", comment: "Label in the feedback form") static let feedbackOtherDescription = NSLocalizedString("feedback.other.description", value: "Please give us your feedback:", comment: "Label in the feedback form") @@ -532,9 +532,9 @@ struct UserText { static let cookiesManagedNotification = NSLocalizedString("notification.badge.cookiesmanaged", value: "Cookies Managed", comment: "Notification that appears when browser automatically handle cookies") static let autoconsentModalTitle = NSLocalizedString("autoconsent.modal.title", value: "Looks like this site has a cookie consent pop-up 👇", comment: "Title for modal asking the user to auto manage cookies") - + static let autoconsentModalBody = NSLocalizedString("autoconsent.modal.body", value: "Want me to handle these for you? I can try to minimize cookies, maximize privacy, and hide pop-ups like these.", comment: "Body for modal asking the user to auto manage cookies") - + static let autoconsentModalConfirmButton = NSLocalizedString("autoconsent.modal.cta.confirm", value: "Manage Cookie Pop-ups", comment: "Confirm button for modal asking the user to auto manage cookies") static let autoconsentModalDenyButton = NSLocalizedString("autoconsent.modal.cta.deny", value: "No Thanks", comment: "Deny button for modal asking the user to auto manage cookies") @@ -575,11 +575,11 @@ struct UserText { static let bitwardenHanshakeNotApproved = NSLocalizedString("bitwarden.handshake.not.approved", value: "Handshake not approved in Bitwarden app", comment: "") static let bitwardenConnecting = NSLocalizedString("bitwarden.connecting", value: "Connecting to Bitwarden", comment: "") static let bitwardenWaitingForStatusResponse = NSLocalizedString("bitwarden.waiting.for.status.response", value: "Waiting for the status response from Bitwarden", comment: "") - + static let connectToBitwarden = NSLocalizedString("bitwarden.connect.title", value: "Connect to Bitwarden", comment: "Title for the Bitwarden onboarding flow") - + static let connectToBitwardenDescription = NSLocalizedString("bitwarden.connect.description", value: "We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo.", comment: "") - + static let connectToBitwardenPrivacy = NSLocalizedString("bitwarden.connect.privacy", value: "Privacy", comment: "") static let installBitwarden = NSLocalizedString("bitwarden.install", value: "Install Bitwarden", comment: "Button to install Bitwarden app") static let installBitwardenInfo = NSLocalizedString("bitwarden.install.info", value: "To begin setup, first install Bitwarden from the App Store.", comment: "Setup of the integration with Bitwarden app") @@ -602,45 +602,45 @@ struct UserText { static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") - + static let showBookmarksShortcut = NSLocalizedString("pinning.show-bookmarks-shortcut", value: "Show Bookmarks Shortcut", comment: "Menu item for showing the bookmarks shortcut") static let hideBookmarksShortcut = NSLocalizedString("pinning.hide-bookmarks-shortcut", value: "Hide Bookmarks Shortcut", comment: "Menu item for hiding the bookmarks shortcut") - + static let showDownloadsShortcut = NSLocalizedString("pinning.show-downloads-shortcut", value: "Show Downloads Shortcut", comment: "Menu item for showing the downloads shortcut") static let hideDownloadsShortcut = NSLocalizedString("pinning.hide-downloads-shortcut", value: "Hide Downloads Shortcut", comment: "Menu item for hiding the downloads shortcut") - + // MARK: - Tooltips - + static let autofillShortcutTooltip = NSLocalizedString("tooltip.autofill.shortcut", value: "Autofill", comment: "Tooltip for the autofill shortcut") static let bookmarksShortcutTooltip = NSLocalizedString("tooltip.bookmarks.shortcut", value: "Bookmarks", comment: "Tooltip for the bookmarks shortcut") static let downloadsShortcutTooltip = NSLocalizedString("tooltip.downloads.shortcut", value: "Downloads", comment: "Tooltip for the downloads shortcut") - + static let addItemTooltip = NSLocalizedString("tooltip.autofill.add-item", value: "Add item", comment: "Tooltip for the Add Item button") static let moreOptionsTooltip = NSLocalizedString("tooltip.autofill.more-options", value: "More options", comment: "Tooltip for the More Options button") - + static let newBookmarkTooltip = NSLocalizedString("tooltip.bookmarks.new-bookmark", value: "New bookmark", comment: "Tooltip for the New Bookmark button") static let newFolderTooltip = NSLocalizedString("tooltip.bookmarks.new-folder", value: "New folder", comment: "Tooltip for the New Folder button") static let manageBookmarksTooltip = NSLocalizedString("tooltip.bookmarks.manage-bookmarks", value: "Manage bookmarks", comment: "Tooltip for the Manage Bookmarks button") - + static let openDownloadsFolderTooltip = NSLocalizedString("tooltip.downloads.open-downloads-folder", value: "Open downloads folder", comment: "Tooltip for the Open Downloads Folder button") static let clearDownloadHistoryTooltip = NSLocalizedString("tooltip.downloads.clear-download-history", value: "Clear download history", comment: "Tooltip for the Clear Downloads button") - + static let newTabTooltip = NSLocalizedString("tooltip.tab.new-tab", value: "Open a new tab", comment: "Tooltip for the New Tab button") static let clearBrowsingHistoryTooltip = NSLocalizedString("tooltip.fire.clear-browsing-history", value: "Clear browsing history", comment: "Tooltip for the Fire button") - + static let navigateBackTooltip = NSLocalizedString("tooltip.navigation.back", value: "Show the previous page\nHold to show history", comment: "Tooltip for the Back button") static let navigateForwardTooltip = NSLocalizedString("tooltip.navigation.forward", value: "Show the next page\nHold to show history", comment: "Tooltip for the Forward button") static let refreshPageTooltip = NSLocalizedString("tooltip.navigation.refresh", value: "Reload this page", comment: "Tooltip for the Refresh button") static let applicationMenuTooltip = NSLocalizedString("tooltip.application-menu.show", value: "Open application menu", comment: "Tooltip for the Application Menu button") - + static let privacyDashboardTooltip = NSLocalizedString("tooltip.privacy-dashboard.show", value: "Show the Privacy Dashboard and manage site settings", comment: "Tooltip for the Privacy Dashboard button") static let addBookmarkTooltip = NSLocalizedString("tooltip.bookmark.add", value: "Bookmark this page", comment: "Tooltip for the Add Bookmark button") static let editBookmarkTooltip = NSLocalizedString("tooltip.bookmark.edit", value: "Edit bookmark", comment: "Tooltip for the Edit Bookmark button") - + static let findInPageCloseTooltip = NSLocalizedString("tooltip.find-in-page.close", value: "Close find bar", comment: "Tooltip for the Find In Page bar's Close button") static let findInPageNextTooltip = NSLocalizedString("tooltip.find-in-page.next", value: "Next result", comment: "Tooltip for the Find In Page bar's Next button") static let findInPagePreviousTooltip = NSLocalizedString("tooltip.find-in-page.previous", value: "Previous result", comment: "Tooltip for the Find In Page bar's Previous button") - + static let copyUsernameTooltip = NSLocalizedString("autofill.copy-username", value: "Copy username", comment: "Tooltip for the Autofill panel's Copy Username button") static let copyPasswordTooltip = NSLocalizedString("autofill.copy-password", value: "Copy password", comment: "Tooltip for the Autofill panel's Copy Password button") static let showPasswordTooltip = NSLocalizedString("autofill.show-password", value: "Show password", comment: "Tooltip for the Autofill panel's Show Password button") @@ -655,7 +655,7 @@ struct UserText { } static let passwordManagerPopoverSettingsButton = NSLocalizedString("autofill.popover.settings-button", value: "Settings", comment: "Open Settings Button") static let passwordManagerPopoverChangeInSettingsLabel = NSLocalizedString("autofill.popover.change-in", value: "Change in", comment: "Suffix of the label - change in settings - ") - + static func passwordManagerPopoverConnectedToUser(user: String) -> String { let localized = NSLocalizedString("autofill.popover.password-manager-connected-to-user", value: "Connected to user %@", comment: "Label describing what user is connected to the password manager") return String(format: localized, user) @@ -665,7 +665,7 @@ struct UserText { let localized = NSLocalizedString("autofill.popover.open-password-manager", value: "Open %@", comment: "Open password manager button") return String(format: localized, managerName) } - + static let passwordManagerLockedStatus = NSLocalizedString("autofill.manager.status.locked", value: "Locked", comment: "Locked status for password manager") static let passwordManagerUnlockedStatus = NSLocalizedString("autofill.manager.status.unlocked", value: "Unlocked", comment: "Unlocked status for password manager") } diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift index 24969422d0..b25b33b536 100644 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ b/DuckDuckGo/Common/Logging/Logging.swift @@ -44,7 +44,7 @@ extension OSLog { static var pixel: OSLog { Logging.pixelLoggingEnabled ? Logging.pixelLog : .disabled } - + static var autoconsent: OSLog { Logging.autoconsentLoggingEnabled ? Logging.autoconsentLog : .disabled } @@ -56,7 +56,7 @@ extension OSLog { static var favicons: OSLog { Logging.faviconLoggingEnabled ? Logging.faviconLog : .disabled } - + static var autoLock: OSLog { Logging.autoLockLoggingEnabled ? Logging.autoLockLog : .disabled } @@ -64,7 +64,7 @@ extension OSLog { static var tabLazyLoading: OSLog { Logging.tabLazyLoaderLoggingEnabled ? Logging.tabLazyLoaderLog : .disabled } - + static var bookmarks: OSLog { Logging.bookmarksLoggingEnabled ? Logging.bookmarksLog : .disabled } @@ -76,7 +76,7 @@ extension OSLog { static var attribution: OSLog { Logging.attributionLoggingEnabled ? Logging.attributionLog : .disabled } - + static var atb: OSLog { Logging.atbLoggingEnabled ? Logging.atbLog : .disabled } @@ -87,7 +87,7 @@ struct Logging { fileprivate static let atbLoggingEnabled = false fileprivate static let atbLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "ATB") - + fileprivate static let configLoggingEnabled = false fileprivate static let configLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Configuration Downloading") @@ -111,7 +111,7 @@ struct Logging { fileprivate static let faviconLoggingEnabled = false fileprivate static let faviconLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Favicons") - + fileprivate static let autoLockLoggingEnabled = false fileprivate static let autoLockLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Auto-Lock") @@ -120,10 +120,10 @@ struct Logging { fileprivate static let autoconsentLoggingEnabled = false fileprivate static let autoconsentLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Autoconsent") - + fileprivate static let bookmarksLoggingEnabled = false fileprivate static let bookmarksLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Bookmarks") - + fileprivate static let attributionLoggingEnabled = false fileprivate static let attributionLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Ad Attribution") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index d733cf11b7..1ec4229a1d 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -90,7 +90,7 @@ public struct UserDefaultsWrapper { case historyV5toV6Migration = "history.v5.to.v6.migration.2" case showBookmarksBar = "bookmarks.bar.show" - + case pinnedViews = "pinning.pinned-views" case lastDatabaseFactoryFailurePixelDate = "last.database.factory.failure.pixel.date" diff --git a/DuckDuckGo/Common/View/AppKit/ColorView.swift b/DuckDuckGo/Common/View/AppKit/ColorView.swift index f7f962c929..e32cbdd48e 100644 --- a/DuckDuckGo/Common/View/AppKit/ColorView.swift +++ b/DuckDuckGo/Common/View/AppKit/ColorView.swift @@ -56,19 +56,19 @@ final class ColorView: NSView { layer?.borderWidth = borderWidth } } - + @IBInspectable var interceptClickEvents: Bool = false func setupView() { self.wantsLayer = true } - + override func updateLayer() { super.updateLayer() layer?.backgroundColor = backgroundColor?.cgColor layer?.borderColor = borderColor?.cgColor } - + // MARK: - Click Event Interception override func mouseDown(with event: NSEvent) { @@ -76,13 +76,13 @@ final class ColorView: NSView { super.mouseDown(with: event) } } - + override func mouseUp(with event: NSEvent) { if !interceptClickEvents { super.mouseUp(with: event) } } - + override func mouseDragged(with event: NSEvent) { if !interceptClickEvents { super.mouseDragged(with: event) diff --git a/DuckDuckGo/Common/View/AppKit/FlatButton.swift b/DuckDuckGo/Common/View/AppKit/FlatButton.swift index ae29bfb984..e876f252bd 100644 --- a/DuckDuckGo/Common/View/AppKit/FlatButton.swift +++ b/DuckDuckGo/Common/View/AppKit/FlatButton.swift @@ -24,12 +24,12 @@ import Foundation @IBInspectable var horizontalPadding: CGFloat = 10 @IBInspectable var verticalPadding: CGFloat = 10 @IBInspectable var backgroundColor: NSColor = .blue - + override func draw(_ dirtyRect: NSRect) { - + self.wantsLayer = true self.layer?.cornerRadius = cornerRadius - + if isHighlighted { layer?.backgroundColor = backgroundColor.blended(withFraction: 0.2, of: .black)?.cgColor } else { diff --git a/DuckDuckGo/Common/View/AppKit/FocusRingView.swift b/DuckDuckGo/Common/View/AppKit/FocusRingView.swift index 31f4a883dd..efe9bc4ffa 100644 --- a/DuckDuckGo/Common/View/AppKit/FocusRingView.swift +++ b/DuckDuckGo/Common/View/AppKit/FocusRingView.swift @@ -97,5 +97,5 @@ final class FocusRingView: NSView { CATransaction.commit() } - + } diff --git a/DuckDuckGo/Common/View/AppKit/GradientView.swift b/DuckDuckGo/Common/View/AppKit/GradientView.swift index d6a106dfc4..21e5dbb76e 100644 --- a/DuckDuckGo/Common/View/AppKit/GradientView.swift +++ b/DuckDuckGo/Common/View/AppKit/GradientView.swift @@ -63,5 +63,5 @@ final class GradientView: NSView { private func setupView() { self.wantsLayer = true } - + } diff --git a/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift b/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift index 755da6de53..d4582ac7ac 100644 --- a/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift +++ b/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift @@ -27,7 +27,7 @@ internal class MouseOverButton: NSButton { updateLayer() } } - + @IBInspectable var mouseOverColor: NSColor? { didSet { updateLayer() diff --git a/DuckDuckGo/Common/View/AppKit/PersistentAppInterfaceSettings.swift b/DuckDuckGo/Common/View/AppKit/PersistentAppInterfaceSettings.swift index 8f74e7e5ce..a9c1cd0487 100644 --- a/DuckDuckGo/Common/View/AppKit/PersistentAppInterfaceSettings.swift +++ b/DuckDuckGo/Common/View/AppKit/PersistentAppInterfaceSettings.swift @@ -20,16 +20,16 @@ import Foundation /// Describes app interface settings that are changed outside of the core Settings interface, but need to be persisted between launches. final class PersistentAppInterfaceSettings { - + static let shared = PersistentAppInterfaceSettings() static let showBookmarksBarSettingChanged = NSNotification.Name("ShowBookmarksBarSettingChanged") - + @UserDefaultsWrapper(key: .showBookmarksBar, defaultValue: false) var showBookmarksBar: Bool { didSet { NotificationCenter.default.post(name: PersistentAppInterfaceSettings.showBookmarksBarSettingChanged, object: nil) } } - + } diff --git a/DuckDuckGo/Common/View/AppKit/WindowDraggingView.swift b/DuckDuckGo/Common/View/AppKit/WindowDraggingView.swift index 9b601ea01f..e50e4f2be0 100644 --- a/DuckDuckGo/Common/View/AppKit/WindowDraggingView.swift +++ b/DuckDuckGo/Common/View/AppKit/WindowDraggingView.swift @@ -35,7 +35,7 @@ final class WindowDraggingView: NSView { super.mouseDown(with: event) return } - + mouseDownSubject.send(event) if event.clickCount == 2 { diff --git a/DuckDuckGo/Common/View/SwiftUI/CustomRoundedCornersShape.swift b/DuckDuckGo/Common/View/SwiftUI/CustomRoundedCornersShape.swift index ac0d7d6848..4a0b51d07c 100644 --- a/DuckDuckGo/Common/View/SwiftUI/CustomRoundedCornersShape.swift +++ b/DuckDuckGo/Common/View/SwiftUI/CustomRoundedCornersShape.swift @@ -22,42 +22,42 @@ struct CustomRoundedCornersShape: Shape, InsettableShape { func inset(by amount: CGFloat) -> CustomRoundedCornersShape { CustomRoundedCornersShape(inset: amount, tl: tl, tr: tr, bl: bl, br: br) } - + typealias InsetShape = CustomRoundedCornersShape - + var inset: CGFloat = 0 var tl: CGFloat = 0.0 var tr: CGFloat = 0.0 var bl: CGFloat = 0.0 var br: CGFloat = 0.0 - + func path(in rect: CGRect) -> Path { var path = Path() - + let effectiveRect = rect.insetBy(dx: inset, dy: inset) - + let w = effectiveRect.size.width let h = effectiveRect.size.height - + // Make sure we do not exceed the size of the rectangle let tr = min(min(self.tr, h/2), w/2) let tl = min(min(self.tl, h/2), w/2) let bl = min(min(self.bl, h/2), w/2) let br = min(min(self.br, h/2), w/2) - + path.move(to: CGPoint(x: w / 2.0, y: 0)) path.addLine(to: CGPoint(x: w - tr, y: 0)) path.addArc(center: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false) - + path.addLine(to: CGPoint(x: w, y: h - br)) path.addArc(center: CGPoint(x: w - br, y: h - br), radius: br, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) - + path.addLine(to: CGPoint(x: bl, y: h)) path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) - + path.addLine(to: CGPoint(x: 0, y: tl)) path.addArc(center: CGPoint(x: tl, y: tl), radius: tl, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false) diff --git a/DuckDuckGo/Common/View/SwiftUI/HoverButton.swift b/DuckDuckGo/Common/View/SwiftUI/HoverButton.swift index d3268c4e13..64a61264f7 100644 --- a/DuckDuckGo/Common/View/SwiftUI/HoverButton.swift +++ b/DuckDuckGo/Common/View/SwiftUI/HoverButton.swift @@ -37,7 +37,7 @@ struct HoverButton: View { imageSize: CGFloat = 16, cornerRadius: CGFloat, action: @escaping () -> Void) { - + self.size = size self.backgroundColor = backgroundColor self.mouseOverColor = mouseOverColor diff --git a/DuckDuckGo/Common/View/SwiftUI/NSPathControlView.swift b/DuckDuckGo/Common/View/SwiftUI/NSPathControlView.swift index 02c2c01594..b08a8ead58 100644 --- a/DuckDuckGo/Common/View/SwiftUI/NSPathControlView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/NSPathControlView.swift @@ -21,14 +21,14 @@ import SwiftUI import Combine struct NSPathControlView: NSViewRepresentable { - + typealias NSViewType = NSPathControl var url: URL? - + func makeNSView(context: NSViewRepresentableContext) -> NSPathControl { let newPathControl = NSPathControl() - + newPathControl.wantsLayer = true newPathControl.isEditable = false newPathControl.refusesFirstResponder = true @@ -53,15 +53,15 @@ struct NSPathControlView: NSViewRepresentable { return newPathControl } - + func updateNSView(_ nsView: NSPathControl, context: NSViewRepresentableContext) { nsView.url = url } - + func makeCoordinator() -> Coordinator { return Coordinator() } - + final class Coordinator { var alphaCancellable: AnyCancellable? var borderColorCancellable: AnyCancellable? diff --git a/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift b/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift index 8980118cab..feffdd46b5 100644 --- a/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift @@ -20,27 +20,27 @@ import AppKit import SwiftUI struct NSPopUpButtonView: NSViewRepresentable where ItemType: Equatable { - + typealias NSViewType = NSPopUpButton @Binding var selection: ItemType var viewCreator: () -> NSPopUpButton - + func makeNSView(context: NSViewRepresentableContext) -> NSPopUpButton { let newPopupButton = viewCreator() setPopUpFromSelection(newPopupButton, selection: selection) - + newPopupButton.target = context.coordinator newPopupButton.action = #selector(Coordinator.dropdownItemSelected(_:)) return newPopupButton } - + func updateNSView(_ nsView: NSPopUpButton, context: NSViewRepresentableContext) { setPopUpFromSelection(nsView, selection: selection) } - + func setPopUpFromSelection(_ button: NSPopUpButton, selection: ItemType) { let itemsList = button.itemArray let matchedMenuItem = itemsList.filter { ($0.representedObject as? ItemType) == selection }.first @@ -49,19 +49,19 @@ struct NSPopUpButtonView: NSViewRepresentable where ItemType: Equatabl button.select(matchedMenuItem) } } - + func makeCoordinator() -> Coordinator { return Coordinator(self) } - + final class Coordinator: NSObject { var parent: NSPopUpButtonView! - + init(_ parent: NSPopUpButtonView) { super.init() self.parent = parent } - + @objc func dropdownItemSelected(_ sender: NSPopUpButton) { guard let selectedItem = sender.selectedItem else { assertionFailure() diff --git a/DuckDuckGo/Common/View/SwiftUI/TextButton.swift b/DuckDuckGo/Common/View/SwiftUI/TextButton.swift index 3852feae6b..97df952725 100644 --- a/DuckDuckGo/Common/View/SwiftUI/TextButton.swift +++ b/DuckDuckGo/Common/View/SwiftUI/TextButton.swift @@ -19,15 +19,15 @@ import SwiftUI struct TextButton: View { - + let title: String let action: () -> Void - + init(_ title: String, action: @escaping () -> Void) { self.title = title self.action = action } - + var body: some View { Button(action: action) { Text(title) diff --git a/DuckDuckGo/Common/View/SwiftUI/ViewExtensions.swift b/DuckDuckGo/Common/View/SwiftUI/ViewExtensions.swift index fa19c266e7..a6c59a3c76 100644 --- a/DuckDuckGo/Common/View/SwiftUI/ViewExtensions.swift +++ b/DuckDuckGo/Common/View/SwiftUI/ViewExtensions.swift @@ -24,7 +24,7 @@ enum ViewVisibility: CaseIterable { case visible, // view is fully visible invisible, // view is hidden but takes up space gone // view is fully removed from the view hierarchy - + } extension View { diff --git a/DuckDuckGo/Configuration/ConfigurationDownloading.swift b/DuckDuckGo/Configuration/ConfigurationDownloading.swift index eb4f4b8257..e5ad1822a7 100644 --- a/DuckDuckGo/Configuration/ConfigurationDownloading.swift +++ b/DuckDuckGo/Configuration/ConfigurationDownloading.swift @@ -45,7 +45,7 @@ enum ConfigurationLocation: String, CaseIterable { case privacyConfiguration = "https://staticcdn.duckduckgo.com/trackerblocking/config/v2/macos-config.json" // In archived repo, to be refactored shortly (https://staticcdn.duckduckgo.com/useragents/social_ctp_configuration.json) case FBConfig = "https://staticcdn.duckduckgo.com/useragents/" - + } final class DefaultConfigurationDownloader: ConfigurationDownloading { @@ -123,7 +123,7 @@ final class DefaultConfigurationDownloader: ConfigurationDownloading { } }) { value in - + promise(.success((value))) }.store(in: &self.cancellables) diff --git a/DuckDuckGo/Configuration/ConfigurationStoring.swift b/DuckDuckGo/Configuration/ConfigurationStoring.swift index b116789d64..209a8e3f02 100644 --- a/DuckDuckGo/Configuration/ConfigurationStoring.swift +++ b/DuckDuckGo/Configuration/ConfigurationStoring.swift @@ -57,7 +57,7 @@ final class DefaultConfigurationStorage: ConfigurationStoring { @UserDefaultsWrapper(key: .configStorageSurrogatesEtag, defaultValue: nil) private var surrogatesEtag: String? - + @UserDefaultsWrapper(key: .configStoragePrivacyConfigurationEtag, defaultValue: nil) private var privacyConfigurationEtag: String? @@ -82,7 +82,7 @@ final class DefaultConfigurationStorage: ConfigurationStoring { case .trackerRadar: return trackerRadarEtag - + case .privacyConfiguration: return privacyConfigurationEtag @@ -107,7 +107,7 @@ final class DefaultConfigurationStorage: ConfigurationStoring { case .trackerRadar: trackerRadarEtag = etag - + case .privacyConfiguration: privacyConfigurationEtag = etag diff --git a/DuckDuckGo/Content Blocker/ClickToLoadModel.swift b/DuckDuckGo/Content Blocker/ClickToLoadModel.swift index 6d4cfcd964..863b9d1018 100644 --- a/DuckDuckGo/Content Blocker/ClickToLoadModel.swift +++ b/DuckDuckGo/Content Blocker/ClickToLoadModel.swift @@ -39,7 +39,7 @@ struct ClickToLoadModel { let image = "data:image/" + (fileExt == "svg" ? "svg+xml" : fileExt) + ";base64," + base64String return image } - + static let getImage: [String: String] = { return [ "dax.png": Self.loadFile(name: "dax.png")!, diff --git a/DuckDuckGo/Content Blocker/ClickToLoadUserScript.swift b/DuckDuckGo/Content Blocker/ClickToLoadUserScript.swift index b9d39f7dc8..5b95773913 100644 --- a/DuckDuckGo/Content Blocker/ClickToLoadUserScript.swift +++ b/DuckDuckGo/Content Blocker/ClickToLoadUserScript.swift @@ -84,7 +84,7 @@ final class ClickToLoadUserScript: NSObject, UserScript, WKScriptMessageHandlerW } replyHandler(image, nil) } - + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { assertionFailure("SHOULDN'T BE HERE!") } diff --git a/DuckDuckGo/Content Blocker/ContentBlockerRulesLists.swift b/DuckDuckGo/Content Blocker/ContentBlockerRulesLists.swift index a53cd1038d..e7ecaa4555 100644 --- a/DuckDuckGo/Content Blocker/ContentBlockerRulesLists.swift +++ b/DuckDuckGo/Content Blocker/ContentBlockerRulesLists.swift @@ -22,11 +22,11 @@ import BrowserServicesKit import CryptoKit final class ContentBlockerRulesLists: DefaultContentBlockerRulesListsSource { - + enum Constants { static let clickToLoadRulesListName = "ClickToLoad" } - + static var fbTrackerDataFile: Data = { do { let url = Bundle.main.url(forResource: "fb-tds", withExtension: "json")! @@ -43,7 +43,7 @@ final class ContentBlockerRulesLists: DefaultContentBlockerRulesListsSource { fatalError("Failed to JSON decode FB-TDS") } }() - + func MD5(data: Data) -> String { let digest = Insecure.MD5.hash(data: data) @@ -51,17 +51,17 @@ final class ContentBlockerRulesLists: DefaultContentBlockerRulesListsSource { String(format: "%02hhx", $0) }.joined() } - + private let adClickAttribution: AdClickAttributing - + init(trackerDataManager: TrackerDataManager, adClickAttribution: AdClickAttributing) { self.adClickAttribution = adClickAttribution super.init(trackerDataManager: trackerDataManager) } - + override var contentBlockerRulesLists: [ContentBlockerRulesList] { var result = super.contentBlockerRulesLists - + if adClickAttribution.isEnabled, let tdsRulesIndex = result.firstIndex(where: { $0.name == DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName }) { let tdsRules = result[tdsRulesIndex] @@ -73,7 +73,7 @@ final class ContentBlockerRulesLists: DefaultContentBlockerRulesListsSource { result.append(splitRules.1) } } - + // Add new ones let etag = MD5(data: Self.fbTrackerDataFile) let dataSet: TrackerDataManager.DataSet = TrackerDataManager.DataSet(Self.fbTrackerDataSet, etag) diff --git a/DuckDuckGo/Content Blocker/ContentBlocking.swift b/DuckDuckGo/Content Blocker/ContentBlocking.swift index cc6ed32fb4..2e6e7e2f26 100644 --- a/DuckDuckGo/Content Blocker/ContentBlocking.swift +++ b/DuckDuckGo/Content Blocker/ContentBlocking.swift @@ -70,7 +70,7 @@ final class AppContentBlocking { data: DefaultConfigurationStorage.shared.loadData(for: .trackerRadar), embeddedDataProvider: AppTrackerDataSetProvider(), errorReporting: Self.debugEvents) - + adClickAttribution = AdClickAttributionFeature(with: privacyConfigurationManager) contentBlockerRulesSource = ContentBlockerRulesLists(trackerDataManager: trackerDataManager, adClickAttribution: adClickAttribution) @@ -87,7 +87,7 @@ final class AppContentBlocking { configStorage: configStorage, privacySecurityPreferences: PrivacySecurityPreferences.shared, tld: tld) - + adClickAttributionRulesProvider = AdClickAttributionRulesProvider(config: adClickAttribution, compiledRulesSource: contentBlockingManager, exceptionsSource: exceptionsSource, @@ -119,10 +119,10 @@ final class AppContentBlocking { case .privacyConfigurationCouldNotBeLoaded: domainEvent = .privacyConfigurationCouldNotBeLoaded - + case .contentBlockingCompilationFailed(let listName, let component): let defaultTDSListName = DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName - + let listType: Pixel.Event.CompileRulesListType switch listName { case defaultTDSListName: @@ -147,7 +147,7 @@ final class AppContentBlocking { Pixel.fire(.debug(event: domainEvent, error: error), withAdditionalParameters: parameters, onComplete: onComplete) } - + // MARK: - Ad Click Attribution let attributionEvents: EventMapping? = .init { event, _, parameters, _ in @@ -161,7 +161,7 @@ final class AppContentBlocking { Pixel.fire(domainEvent, withAdditionalParameters: parameters ?? [:]) } - + let attributionDebugEvents: EventMapping? = .init { event, _, _, _ in let domainEvent: Pixel.Event.Debug switch event { diff --git a/DuckDuckGo/Crash Reports/Model/CrashReport.swift b/DuckDuckGo/Crash Reports/Model/CrashReport.swift index 0b15ac2d01..773c66cd27 100644 --- a/DuckDuckGo/Crash Reports/Model/CrashReport.swift +++ b/DuckDuckGo/Crash Reports/Model/CrashReport.swift @@ -19,7 +19,7 @@ import Foundation protocol CrashReport { - + static var fileExtension: String { get } var url: URL { get } @@ -58,7 +58,7 @@ struct LegacyCrashReport: CrashReport { } struct JSONCrashReport: CrashReport { - + static let fileExtension = "ips" static let headerItemsToFilter = [ @@ -68,12 +68,12 @@ struct JSONCrashReport: CrashReport { ] let url: URL - + var content: String? { guard var fileContents = try? String(contentsOf: url) else { return nil } - + for itemToFilter in Self.headerItemsToFilter { let patternToReplace = "\"\(itemToFilter)\"\\s*:\\s*\"[^\"]*\"" let redactedKeyValuePair = "\"\(itemToFilter)\":\"\"" @@ -82,10 +82,10 @@ struct JSONCrashReport: CrashReport { with: redactedKeyValuePair, options: .regularExpression) } - + return fileContents } - + var contentData: Data? { content?.data(using: .utf8) } diff --git a/DuckDuckGo/Crash Reports/Model/CrashReportReader.swift b/DuckDuckGo/Crash Reports/Model/CrashReportReader.swift index d300967cd9..feae2db45a 100644 --- a/DuckDuckGo/Crash Reports/Model/CrashReportReader.swift +++ b/DuckDuckGo/Crash Reports/Model/CrashReportReader.swift @@ -57,7 +57,7 @@ final class CrashReportReader { return creationDate > lastCheckDate && creationDate < Date() } - + private func crashReport(from url: URL) -> CrashReport? { switch url.pathExtension { case LegacyCrashReport.fileExtension: return LegacyCrashReport(url: url) diff --git a/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift b/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift index a1d9822621..e6ac958fc3 100644 --- a/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift +++ b/DuckDuckGo/Crash Reports/Model/CrashReportSender.swift @@ -21,7 +21,7 @@ import Foundation final class CrashReportSender { static let reportServiceUrl = URL(string: "https://duckduckgo.com/crash.js")! - + private let session = URLSession(configuration: .ephemeral) func send(_ crashReport: CrashReport) { diff --git a/DuckDuckGo/Crash Reports/View/CrashReportPromptViewController.swift b/DuckDuckGo/Crash Reports/View/CrashReportPromptViewController.swift index fcf03413a2..1d6c3fe12e 100644 --- a/DuckDuckGo/Crash Reports/View/CrashReportPromptViewController.swift +++ b/DuckDuckGo/Crash Reports/View/CrashReportPromptViewController.swift @@ -54,5 +54,5 @@ final class CrashReportPromptViewController: NSViewController { delegate?.crashReportPromptViewController(self, userDidAllowToReport: false) view.window?.close() } - + } diff --git a/DuckDuckGo/Data Import/Bookmarks/BookmarkImport.swift b/DuckDuckGo/Data Import/Bookmarks/BookmarkImport.swift index f28dbc3d7b..1395e15e7a 100644 --- a/DuckDuckGo/Data Import/Bookmarks/BookmarkImport.swift +++ b/DuckDuckGo/Data Import/Bookmarks/BookmarkImport.swift @@ -21,7 +21,7 @@ import Foundation enum BookmarkImportSource: Equatable { case duckduckgoWebKit case thirdPartyBrowser(DataImport.Source) - + var importSourceName: String { switch self { case .duckduckgoWebKit: return UserText.importBookmarksHTML diff --git a/DuckDuckGo/Data Import/Bookmarks/Chromium/ChromiumFaviconsReader.swift b/DuckDuckGo/Data Import/Bookmarks/Chromium/ChromiumFaviconsReader.swift index a30e1ea26f..cb36498bf1 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Chromium/ChromiumFaviconsReader.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Chromium/ChromiumFaviconsReader.swift @@ -30,17 +30,17 @@ final class ChromiumFaviconsReader { case failedToTemporarilyCopyFile case unexpectedFaviconsDatabaseFormat } - + final class ChromiumFavicon: FetchableRecord { let pageURL: String let iconURL: String let size: Int let imageData: Data - + var image: NSImage? { NSImage(data: imageData) } - + init(row: Row) { pageURL = row["page_url"] iconURL = row["url"] @@ -75,12 +75,12 @@ final class ChromiumFaviconsReader { guard let favicons = try? ChromiumFavicon.fetchAll(database, sql: allFaviconsQuery()) else { throw ImportError.unexpectedFaviconsDatabaseFormat } - + return favicons } - + let faviconsByURL = Dictionary(grouping: favicons, by: { $0.pageURL }) - + return .success(faviconsByURL) } catch { return .failure(.unexpectedFaviconsDatabaseFormat) diff --git a/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxBookmarksReader.swift b/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxBookmarksReader.swift index 4f48d77804..711e7879e7 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxBookmarksReader.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxBookmarksReader.swift @@ -89,7 +89,7 @@ final class FirefoxBookmarksReader { return .failure(.unexpectedBookmarksDatabaseFormat) } } - + fileprivate class DatabaseBookmarks { let topLevelFolders: [FolderRow] let foldersByParent: [Int: [FolderRow]] @@ -146,7 +146,7 @@ final class FirefoxBookmarksReader { let unfiledFolder = ImportedBookmarks.BookmarkOrFolder(name: "other", type: "folder", urlString: nil, children: unfiledBookmarksAndFolders) let folders = ImportedBookmarks.TopLevelFolders(bookmarkBar: toolbarFolder, otherBookmarks: unfiledFolder) - + return ImportedBookmarks(topLevelFolders: folders) } diff --git a/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxFaviconsReader.swift b/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxFaviconsReader.swift index 1f612524da..7d9cfb5053 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxFaviconsReader.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Firefox/FirefoxFaviconsReader.swift @@ -30,17 +30,17 @@ final class FirefoxFaviconsReader { case failedToTemporarilyCopyFile case unexpectedFaviconsDatabaseFormat } - + final class FirefoxFavicon: FetchableRecord { let pageURL: String let iconURL: String let size: Int let imageData: Data - + var image: NSImage? { NSImage(data: imageData) } - + init(row: Row) { pageURL = row["page_url"] iconURL = row["icon_url"] @@ -75,12 +75,12 @@ final class FirefoxFaviconsReader { guard let favicons = try? FirefoxFavicon.fetchAll(database, sql: allFaviconsQuery()) else { throw ImportError.unexpectedFaviconsDatabaseFormat } - + return favicons } - + let faviconsByURL = Dictionary(grouping: favicons, by: { $0.pageURL }) - + return .success(faviconsByURL) } catch { return .failure(.unexpectedFaviconsDatabaseFormat) diff --git a/DuckDuckGo/Data Import/Bookmarks/Safari/SafariDataImporter.swift b/DuckDuckGo/Data Import/Bookmarks/Safari/SafariDataImporter.swift index 5e98ab4dcc..7e25dbe3e4 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Safari/SafariDataImporter.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Safari/SafariDataImporter.swift @@ -36,12 +36,12 @@ internal class SafariDataImporter: DataImporter { _ = openPanel.runModal() return openPanel.urls.first } - + static private var safariDataDirectoryURL: URL { let applicationSupport = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first! return applicationSupport.appendingPathComponent("Safari/") } - + static private var bookmarksFileURL: URL { return safariDataDirectoryURL.appendingPathComponent("Bookmarks.plist") } @@ -69,7 +69,7 @@ internal class SafariDataImporter: DataImporter { let faviconsReader = SafariFaviconsReader(safariDataDirectoryURL: Self.safariDataDirectoryURL) let faviconsResult = faviconsReader.readFavicons() - + switch faviconsResult { case .success(let faviconsByURL): for (pageURLString, fetchedFavicons) in faviconsByURL { @@ -86,7 +86,7 @@ internal class SafariDataImporter: DataImporter { faviconManager.handleFavicons(favicons, documentUrl: pageURL) } } - + case .failure: Pixel.fire(.faviconImportFailed(source: .safari)) } diff --git a/DuckDuckGo/Data Import/Bookmarks/Safari/SafariFaviconsReader.swift b/DuckDuckGo/Data Import/Bookmarks/Safari/SafariFaviconsReader.swift index d67cbde01a..8d125136b8 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Safari/SafariFaviconsReader.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Safari/SafariFaviconsReader.swift @@ -31,15 +31,15 @@ final class SafariFaviconsReader { case failedToTemporarilyCopyFile case unexpectedFaviconsDatabaseFormat } - + fileprivate final class SafariFaviconRecord: FetchableRecord { let host: String - + var formattedHost: String? { guard let url = URL(string: host) else { return nil } - + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil } @@ -47,7 +47,7 @@ final class SafariFaviconsReader { components.scheme = "https" components.host = components.path components.path = "" - + return components.string } @@ -55,15 +55,15 @@ final class SafariFaviconsReader { host = row["host"] } } - + final class SafariFavicon { let host: String let imageData: Data - + var image: NSImage? { NSImage(data: imageData) } - + fileprivate init(host: String, imageData: Data) { self.host = host self.imageData = imageData @@ -98,10 +98,10 @@ final class SafariFaviconsReader { guard let records = try? SafariFaviconRecord.fetchAll(database, sql: allFaviconsQuery()) else { throw ImportError.unexpectedFaviconsDatabaseFormat } - + return records } - + let favicons: [SafariFavicon] = faviconRecords.compactMap { record in guard let imageData = fetchImageData(with: record.host) else { return nil @@ -115,13 +115,13 @@ final class SafariFaviconsReader { } let faviconsByURL = Dictionary(grouping: favicons, by: { $0.host }) - + return .success(faviconsByURL) } catch { return .failure(.unexpectedFaviconsDatabaseFormat) } } - + private func fetchImageData(with host: String) -> Data? { guard let hostData = host.data(using: .utf8) else { return nil @@ -134,7 +134,7 @@ final class SafariFaviconsReader { let faviconsDirectoryURL = safariFaviconsDatabaseURL.deletingLastPathComponent() let faviconURL = faviconsDirectoryURL.appendingPathComponent("Images").appendingPathComponent(hash).appendingPathExtension("png") - + return try? Data(contentsOf: faviconURL) } diff --git a/DuckDuckGo/Data Import/ChromePreferences.swift b/DuckDuckGo/Data Import/ChromePreferences.swift index 06eab74dc4..646ace214f 100644 --- a/DuckDuckGo/Data Import/ChromePreferences.swift +++ b/DuckDuckGo/Data Import/ChromePreferences.swift @@ -19,11 +19,11 @@ import AppKit struct ChromePreferences: Codable { - + struct ChromeProfile: Codable { let name: String } - + let profile: ChromeProfile - + } diff --git a/DuckDuckGo/Data Import/DataImport.swift b/DuckDuckGo/Data Import/DataImport.swift index 609ebd3dd2..764430e419 100644 --- a/DuckDuckGo/Data Import/DataImport.swift +++ b/DuckDuckGo/Data Import/DataImport.swift @@ -236,7 +236,7 @@ struct DataImportError: Error, Equatable { case bookmarks case logins case generic - + var pixelEventAction: Pixel.Event.DataImportAction { switch self { case .bookmarks: return .importBookmarks @@ -245,7 +245,7 @@ struct DataImportError: Error, Equatable { } } } - + enum ImportErrorType: Equatable { case noFileFound case cannotFindFile @@ -260,7 +260,7 @@ struct DataImportError: Error, Equatable { case cannotDecryptFile case failedToTemporarilyCopyFile case databaseAccessFailed - + var stringValue: String { switch self { case .couldNotAccessKeychain: return "couldNotAccessKeychain" @@ -278,10 +278,10 @@ struct DataImportError: Error, Equatable { .databaseAccessFailed: return String(describing: self) } } - + var errorParameters: [String: String] { var parameters = ["error": stringValue] - + switch self { case .couldNotAccessKeychain(let status): parameters["keychainErrorCode"] = String(status) case .noFileFound, @@ -297,11 +297,11 @@ struct DataImportError: Error, Equatable { .failedToTemporarilyCopyFile, .databaseAccessFailed: break } - + return parameters } } - + static func generic(_ errorType: ImportErrorType) -> DataImportError { return DataImportError(actionType: .generic, errorType: errorType) } @@ -319,7 +319,7 @@ struct DataImportError: Error, Equatable { case .failedToTemporarilyCopyFile: return DataImportError(actionType: .bookmarks, errorType: .failedToTemporarilyCopyFile) } } - + static func bookmarks(_ errorType: SafariBookmarksReader.ImportError) -> DataImportError { switch errorType { case .unexpectedBookmarksFileFormat: return DataImportError(actionType: .bookmarks, errorType: .cannotReadFile) @@ -333,11 +333,11 @@ struct DataImportError: Error, Equatable { } // MARK: Login Error Types - + static func logins(_ errorType: ImportErrorType) -> DataImportError { return DataImportError(actionType: .logins, errorType: errorType) } - + static func logins(_ errorType: ChromiumLoginReader.ImportError) -> DataImportError { switch errorType { case .decryptionKeyAccessFailed(let status): return DataImportError(actionType: .logins, errorType: .couldNotAccessKeychain(status)) @@ -349,7 +349,7 @@ struct DataImportError: Error, Equatable { case .userDeniedKeychainPrompt: return DataImportError(actionType: .logins, errorType: .userDeniedKeychainPrompt) } } - + let actionType: ImportErrorAction let errorType: ImportErrorType diff --git a/DuckDuckGo/Data Import/Logins/Chromium/BraveDataImporter.swift b/DuckDuckGo/Data Import/Logins/Chromium/BraveDataImporter.swift index c6d86ddf4c..a0e96729e8 100644 --- a/DuckDuckGo/Data Import/Logins/Chromium/BraveDataImporter.swift +++ b/DuckDuckGo/Data Import/Logins/Chromium/BraveDataImporter.swift @@ -23,7 +23,7 @@ final class BraveDataImporter: ChromiumDataImporter { override var processName: String { return "Brave" } - + override var source: DataImport.Source { return .brave } diff --git a/DuckDuckGo/Data Import/Logins/Chromium/ChromeDataImporter.swift b/DuckDuckGo/Data Import/Logins/Chromium/ChromeDataImporter.swift index a1ed70b09d..b1a9b7315b 100644 --- a/DuckDuckGo/Data Import/Logins/Chromium/ChromeDataImporter.swift +++ b/DuckDuckGo/Data Import/Logins/Chromium/ChromeDataImporter.swift @@ -23,7 +23,7 @@ final class ChromeDataImporter: ChromiumDataImporter { override var processName: String { return "Chrome" } - + override var source: DataImport.Source { return .chrome } diff --git a/DuckDuckGo/Data Import/Logins/Chromium/ChromiumDataImporter.swift b/DuckDuckGo/Data Import/Logins/Chromium/ChromiumDataImporter.swift index 3b2df9fe3a..2155fc234e 100644 --- a/DuckDuckGo/Data Import/Logins/Chromium/ChromiumDataImporter.swift +++ b/DuckDuckGo/Data Import/Logins/Chromium/ChromiumDataImporter.swift @@ -23,7 +23,7 @@ internal class ChromiumDataImporter: DataImporter { var processName: String { fatalError("Subclasses must provide their own process name") } - + var source: DataImport.Source { fatalError("Subclasses must return a source") } @@ -47,7 +47,7 @@ internal class ChromiumDataImporter: DataImporter { func importData(types: [DataImport.DataType], from profile: DataImport.BrowserProfile?, completion: @escaping (Result) -> Void) { - + var summary = DataImport.Summary() let dataDirectoryURL = profile?.profileURL ?? applicationDataDirectoryURL @@ -74,7 +74,7 @@ internal class ChromiumDataImporter: DataImporter { let bookmarkResult = bookmarkReader.readBookmarks() importFavicons(from: dataDirectoryURL) - + switch bookmarkResult { case .success(let bookmarks): do { @@ -99,7 +99,7 @@ internal class ChromiumDataImporter: DataImporter { completion(.success(summary)) } - + func importFavicons(from dataDirectoryURL: URL) { let faviconsReader = ChromiumFaviconsReader(chromiumDataDirectoryURL: dataDirectoryURL) let faviconsResult = faviconsReader.readFavicons() @@ -116,11 +116,11 @@ internal class ChromiumDataImporter: DataImporter { documentUrl: pageURL, dateCreated: Date()) } - + faviconManager.handleFavicons(favicons, documentUrl: pageURL) } } - + case .failure: Pixel.fire(.faviconImportFailed(source: self.source.pixelEventSource)) } diff --git a/DuckDuckGo/Data Import/Logins/Chromium/ChromiumKeychainPrompt.swift b/DuckDuckGo/Data Import/Logins/Chromium/ChromiumKeychainPrompt.swift index 288342ef0c..3c30a50f90 100644 --- a/DuckDuckGo/Data Import/Logins/Chromium/ChromiumKeychainPrompt.swift +++ b/DuckDuckGo/Data Import/Logins/Chromium/ChromiumKeychainPrompt.swift @@ -57,5 +57,5 @@ final class ChromiumKeychainPrompt: ChromiumKeychainPrompting { return .keychainError(status) } } - + } diff --git a/DuckDuckGo/Data Import/Logins/Chromium/ChromiumLoginReader.swift b/DuckDuckGo/Data Import/Logins/Chromium/ChromiumLoginReader.swift index 37a52d2947..bacd0eb6de 100644 --- a/DuckDuckGo/Data Import/Logins/Chromium/ChromiumLoginReader.swift +++ b/DuckDuckGo/Data Import/Logins/Chromium/ChromiumLoginReader.swift @@ -55,12 +55,12 @@ final class ChromiumLoginReader { func readLogins() -> Result<[ImportedLoginCredential], ChromiumLoginReader.ImportError> { let key: String - + if let decryptionKey = decryptionKey { key = decryptionKey } else { let keyPromptResult = decryptionKeyPrompt.promptForChromiumPasswordKeychainAccess(processName: processName) - + switch keyPromptResult { case .password(let passwordString): key = passwordString case .failedToDecodePasswordData: return .failure(.failedToDecodePasswordData) @@ -68,14 +68,14 @@ final class ChromiumLoginReader { case .keychainError(let status): return .failure(.decryptionKeyAccessFailed(status)) } } - + guard let derivedKey = deriveKey(from: key) else { return .failure(.decryptionFailed) } return readLogins(using: derivedKey) } - + private func readLogins(using key: Data) -> Result<[ImportedLoginCredential], ChromiumLoginReader.ImportError> { let loginFileURLs = [chromiumLocalLoginDirectoryURL, chromiumGoogleAccountLoginDirectoryURL] .filter { FileManager.default.fileExists(atPath: $0.path) } @@ -88,7 +88,7 @@ final class ChromiumLoginReader { for loginFileURL in loginFileURLs { let result = readLoginRows(loginFileURL: loginFileURL) - + switch result { case .success(let newLoginRows): loginRows.merge(newLoginRows) { existingRow, newRow in @@ -106,17 +106,17 @@ final class ChromiumLoginReader { let importedLogins = createImportedLoginCredentials(from: loginRows.values, decryptionKey: key) return .success(importedLogins) } - + private func readLoginRows(loginFileURL: URL) -> Result<[ChromiumCredential.ID: ChromiumCredential], ChromiumLoginReader.ImportError> { let temporaryFileHandler = TemporaryFileHandler(fileURL: loginFileURL) defer { temporaryFileHandler.deleteTemporarilyCopiedFile() } - + guard let temporaryDatabaseURL = try? temporaryFileHandler.copyFileToTemporaryDirectory() else { return .failure(.failedToTemporarilyCopyDatabase) } - + var loginRows = [ChromiumCredential.ID: ChromiumCredential]() - + do { let queue = try DatabaseQueue(path: temporaryDatabaseURL.path) var rows = [ChromiumCredential]() @@ -142,17 +142,17 @@ final class ChromiumLoginReader { } catch { return .failure(.databaseAccessFailed) } - + return .success(loginRows) } - + private func createImportedLoginCredentials(from credentials: Dictionary.Values, decryptionKey: Data) -> [ImportedLoginCredential] { return credentials.compactMap { row -> ImportedLoginCredential? in guard let decryptedPassword = decrypt(passwordData: row.encryptedPassword, with: decryptionKey) else { return nil } - + return ImportedLoginCredential( url: row.url, username: row.username, @@ -160,7 +160,7 @@ final class ChromiumLoginReader { ) } } - + private func fetchCredentials(from database: GRDB.Database) throws -> [ChromiumCredential] { do { return try ChromiumCredential.fetchAll(database, sql: Self.sqlSelectWithPasswordTimestamp) diff --git a/DuckDuckGo/Data Import/Logins/Chromium/EdgeDataImporter.swift b/DuckDuckGo/Data Import/Logins/Chromium/EdgeDataImporter.swift index c795f2697e..026e81388b 100644 --- a/DuckDuckGo/Data Import/Logins/Chromium/EdgeDataImporter.swift +++ b/DuckDuckGo/Data Import/Logins/Chromium/EdgeDataImporter.swift @@ -23,7 +23,7 @@ final class EdgeDataImporter: ChromiumDataImporter { override var processName: String { return "Microsoft Edge" } - + override var source: DataImport.Source { return .edge } diff --git a/DuckDuckGo/Data Import/Logins/Firefox/ASN1Parser.swift b/DuckDuckGo/Data Import/Logins/Firefox/ASN1Parser.swift index 0dfcaa7da9..fbadd365c6 100644 --- a/DuckDuckGo/Data Import/Logins/Firefox/ASN1Parser.swift +++ b/DuckDuckGo/Data Import/Logins/Firefox/ASN1Parser.swift @@ -57,9 +57,9 @@ private class Scanner { guard length > 0 else { return Data() } - + let adjustedIndex = data.startIndex + index - + guard adjustedIndex + length <= data.endIndex else { throw ScannerError.outOfBounds } diff --git a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxBerkeleyDatabaseReader.m b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxBerkeleyDatabaseReader.m index d1691a747b..71baddd4e1 100644 --- a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxBerkeleyDatabaseReader.m +++ b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxBerkeleyDatabaseReader.m @@ -64,23 +64,23 @@ @implementation FirefoxBerkeleyDatabaseReader DB *db = dbopen(path, O_RDONLY, O_RDONLY, DB_HASH, NULL); NSMutableDictionary *resultDictionary = [NSMutableDictionary dictionary]; DBT currentKeyDBT, currentDataDBT; - + while (db->seq(db, ¤tKeyDBT, ¤tDataDBT, R_NEXT) == 0) { NSData *currentKeyData = [NSData dataWithBytes:currentKeyDBT.data length:currentKeyDBT.size]; NSData *currentData = [NSData dataWithBytes:currentDataDBT.data length:currentDataDBT.size]; - + NSString *currentKeyHexadecimalString = [currentKeyData hexadecimalString]; NSString *currentKeyString = [[NSString alloc] initWithData:currentKeyData encoding:NSUTF8StringEncoding]; - + if ([currentKeyHexadecimalString isEqualToString:FirefoxBerkeleyDatabaseReaderASN1Key]) { [resultDictionary setValue:currentData forKey:@"data"]; } else { [resultDictionary setValue:currentData forKey:currentKeyString]; } } - + db->close(db); - + return resultDictionary; } diff --git a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxDataImporter.swift b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxDataImporter.swift index e7052e8d13..eec728825b 100644 --- a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxDataImporter.swift +++ b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxDataImporter.swift @@ -89,7 +89,7 @@ final class FirefoxDataImporter: DataImporter { completion(.failure(.bookmarks(.unexpectedBookmarksDatabaseFormat))) return } - + completion(.failure(.bookmarks(error))) return } @@ -101,7 +101,7 @@ final class FirefoxDataImporter: DataImporter { completion(.success(summary)) } - + func importData(types: [DataImport.DataType], from profile: DataImport.BrowserProfile?) async -> Result { return await withCheckedContinuation { continuation in importData(types: types, from: profile) { result in @@ -109,11 +109,11 @@ final class FirefoxDataImporter: DataImporter { } } } - + private func importFavicons(from firefoxProfileURL: URL) { let faviconsReader = FirefoxFaviconsReader(firefoxDataDirectoryURL: firefoxProfileURL) let faviconsResult = faviconsReader.readFavicons() - + switch faviconsResult { case .success(let faviconsByURL): for (pageURLString, fetchedFavicons) in faviconsByURL { @@ -126,11 +126,11 @@ final class FirefoxDataImporter: DataImporter { documentUrl: pageURL, dateCreated: Date()) } - + faviconManager.handleFavicons(favicons, documentUrl: pageURL) } } - + case .failure: Pixel.fire(.faviconImportFailed(source: .firefox)) } diff --git a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxEncryptionKeyReader.swift b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxEncryptionKeyReader.swift index c8429ca6ef..38792be135 100644 --- a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxEncryptionKeyReader.swift +++ b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxEncryptionKeyReader.swift @@ -29,44 +29,44 @@ protocol FirefoxEncryptionKeyReading { } final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { - + func getEncryptionKey(key3DatabaseURL: URL, primaryPassword: String) -> Result { guard let result = FirefoxBerkeleyDatabaseReader.readDatabase(key3DatabaseURL.path) else { return .failure(.databaseAccessFailed) } - + guard let globalSalt = result["global-salt"], let asnData = result["data"]?[4...] else { // Drop the first 4 bytes, they aren't required for decryption and can be ignored return .failure(.decryptionFailed) } - + // Part 1: Take the data from the database and decrypt it. - + guard let decodedASNData = try? ASN1Parser.parse(data: asnData), let entrySalt = extractKey3EntrySalt(from: decodedASNData), let ciphertext = extractCiphertext(from: decodedASNData)else { return .failure(.decryptionFailed) } - + guard let decryptedData = tripleDesDecrypt(ciphertext: ciphertext, globalSalt: globalSalt, entrySalt: entrySalt, primaryPassword: primaryPassword) else { return .failure(.decryptionFailed) } - + // Part 2: Take the decrypted ASN1 data, parse it, and extract the key. - + guard let decryptedASNData = try? ASN1Parser.parse(data: decryptedData), let extractedASNData = extractKey3DecryptedASNData(from: decryptedASNData), let keyContainerASNData = try? ASN1Parser.parse(data: extractedASNData), let key = extractKey3Key(from: keyContainerASNData) else { return .failure(.decryptionFailed) } - + return .success(key) } - + func getEncryptionKey(key4DatabaseURL: URL, primaryPassword: String) -> Result { do { return try key4DatabaseURL.withTemporaryFile { temporaryDatabaseURL in @@ -88,7 +88,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { return .failure(.failedToTemporarilyCopyFile) } } - + private func getKey(key4DatabaseURL databaseURL: URL, primaryPassword: String) throws -> Data? { let queue = try DatabaseQueue(path: databaseURL.path) @@ -103,7 +103,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { guard let decodedASNData = try? ASN1Parser.parse(data: item2) else { throw FirefoxLoginReader.ImportError.decryptionFailed } - + if let tripleDesData = try extractKeyUsing3DES(from: decodedASNData, globalSalt: globalSalt, primaryPassword: primaryPassword, @@ -119,7 +119,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { } // MARK: - Key3 Database Parsing - + private func extractKey3EntrySalt(from tlv: ASN1Parser.Node) -> Data? { guard case let .sequence(outerSequence) = tlv, let firstSequence = outerSequence[safe: 0], @@ -130,30 +130,30 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { case let .octetString(data: data) = octetString else { return nil } - + return data } - + private func extractKey3DecryptedASNData(from node: ASN1Parser.Node) -> Data? { guard case let .sequence(outerSequence) = node, let octetString = outerSequence[safe: 2], case let .octetString(data: data) = octetString else { return nil } - + return data } - + private func extractKey3Key(from node: ASN1Parser.Node) -> Data? { guard case let .sequence(outerSequence) = node, let integer = outerSequence[safe: 3], case let .integer(data: data) = integer else { return nil } - + return data } - + /// HP = SHA1( global-salt | PrimaryPassword ) /// CHP = SHA1( HP | ES ) /// k1 = HMAC-SHA1( key=CHP, msg= (PES | ES) ) @@ -172,7 +172,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { let pointer = rawBufferPointer.baseAddress! pes.replaceBytes(in: NSRange(location: 0, length: entrySalt.count), withBytes: pointer) } - + let hp = SHA.from(data: globalSalt + primaryPasswordData) let chp = SHA.from(data: hp + entrySalt) let k1 = calculateHMAC(key: chp, message: (pes + entrySalt)) @@ -182,19 +182,19 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { let key = k.prefix(24) let iv = k.suffix(8) - + return Cryptography.decrypt3DES(data: ciphertext, key: key, iv: iv) } - + private func calculateHMAC(key: Data, message: Data) -> Data { let symmetricKey = SymmetricKey(data: key) let authentication = CryptoKit.HMAC.authenticationCode(for: message, using: symmetricKey) - + return Data(authentication) } // MARK: - Key4 Database Parsing - + private func extractKey4EntrySalt(from tlv: ASN1Parser.Node) -> Data? { guard case let .sequence(values1) = tlv, let firstValue = values1[safe: 0], @@ -273,7 +273,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { return data } - + private func aesDecrypt(tlv: ASN1Parser.Node, iv: Data, globalSalt: Data, @@ -302,36 +302,36 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { return Data(decryptedData) } - + // MARK: - ASN Key Extraction - + private func extractKeyUsing3DES(from node: ASN1Parser.Node, globalSalt: Data, primaryPassword: String, database: GRDB.Database) throws -> Data? { guard let entrySalt = extractKey3EntrySalt(from: node), let passwordCheckCiphertext = extractCiphertext(from: node) else { return nil } - + guard let decryptedCiphertext = tripleDesDecrypt(ciphertext: passwordCheckCiphertext, globalSalt: globalSalt, entrySalt: entrySalt, primaryPassword: primaryPassword) else { return nil } - + let passwordCheckString = String(data: decryptedCiphertext, encoding: .utf8) - + if passwordCheckString != "password-check" { throw FirefoxLoginReader.ImportError.requiresPrimaryPassword } - + guard let nssPrivateRow = try? Row.fetchOne(database, sql: "SELECT a11, a102 FROM nssPrivate;") else { throw FirefoxLoginReader.ImportError.decryptionFailed } let a11: Data = nssPrivateRow["a11"] let a102: Data = nssPrivateRow["a102"] - + assert(a102 == Data([248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])) - + guard let decodedA11 = try? ASN1Parser.parse(data: a11), let entrySalt = extractKey3EntrySalt(from: decodedA11), let ciphertext = extractCiphertext(from: decodedA11) else { @@ -340,7 +340,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { return tripleDesDecrypt(ciphertext: ciphertext, globalSalt: globalSalt, entrySalt: entrySalt, primaryPassword: primaryPassword) } - + private func extractKeyUsingAES(from node: ASN1Parser.Node, globalSalt: Data, primaryPassword: String, database: GRDB.Database) throws -> Data? { guard let iv = extractInitializationVector(from: node), let decryptedItem2 = aesDecrypt(tlv: node, iv: iv, globalSalt: globalSalt, primaryPassword: primaryPassword) else { diff --git a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxLoginReader.swift b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxLoginReader.swift index 6f31991456..1b4a0c995e 100644 --- a/DuckDuckGo/Data Import/Logins/Firefox/FirefoxLoginReader.swift +++ b/DuckDuckGo/Data Import/Logins/Firefox/FirefoxLoginReader.swift @@ -39,7 +39,7 @@ final class FirefoxLoginReader { enum DataFormat: CaseIterable { case version3 case version2 - + var formatFileNames: (databaseName: String, loginFileName: String) { switch self { case .version3: return (databaseName: "key4.db", loginFileName: "logins.json") @@ -67,7 +67,7 @@ final class FirefoxLoginReader { func readLogins(dataFormat: DataFormat?) -> Result<[ImportedLoginCredential], FirefoxLoginReader.ImportError> { var detectedFormat: DataFormat? - + if let dataFormat = dataFormat { detectedFormat = dataFormat } else { @@ -75,7 +75,7 @@ final class FirefoxLoginReader { for potentialFormat in DataFormat.allCases { let potentialDatabaseURL = firefoxProfileURL.appendingPathComponent(potentialFormat.formatFileNames.databaseName) let potentialLoginsFileURL = firefoxProfileURL.appendingPathComponent(potentialFormat.formatFileNames.loginFileName) - + if FileManager.default.fileExists(atPath: potentialDatabaseURL.path), FileManager.default.fileExists(atPath: potentialLoginsFileURL.path) { detectedFormat = potentialFormat @@ -83,20 +83,20 @@ final class FirefoxLoginReader { } } } - + guard let detectedFormat = detectedFormat else { return .failure(.couldNotFindLoginsFile) } - + let databaseURL = firefoxProfileURL.appendingPathComponent(detectedFormat.formatFileNames.databaseName) let loginsFileURL = firefoxProfileURL.appendingPathComponent(detectedFormat.formatFileNames.loginFileName) - + // If there isn't a file where logins are expected, consider it a successful import of 0 logins // to avoid showing an error state. guard FileManager.default.fileExists(atPath: loginsFileURL.path) else { return .success([]) } - + guard let logins = readLoginsFile(from: loginsFileURL.path) else { return .failure(.couldNotReadLoginsFile) } @@ -107,7 +107,7 @@ final class FirefoxLoginReader { case .version2: encryptionKeyResult = keyReader.getEncryptionKey(key3DatabaseURL: databaseURL, primaryPassword: primaryPassword ?? "") case .version3: encryptionKeyResult = keyReader.getEncryptionKey(key4DatabaseURL: databaseURL, primaryPassword: primaryPassword ?? "") } - + switch encryptionKeyResult { case .success(let keyData): let decryptedLogins = decrypt(logins: logins, with: keyData) diff --git a/DuckDuckGo/Data Import/ThirdPartyBrowser.swift b/DuckDuckGo/Data Import/ThirdPartyBrowser.swift index c739b6f125..1675408e34 100644 --- a/DuckDuckGo/Data Import/ThirdPartyBrowser.swift +++ b/DuckDuckGo/Data Import/ThirdPartyBrowser.swift @@ -86,7 +86,7 @@ enum ThirdPartyBrowser: CaseIterable { return NSWorkspace.shared.icon(forFile: applicationPath) } - + /// Used when specific apps are not installed, but still need to be displayed in the list. /// Browsers are hidden when not installed, so this only applies to password managers. var fallbackApplicationIcon: NSImage? { @@ -137,10 +137,10 @@ enum ThirdPartyBrowser: CaseIterable { $0.forceTerminate() } } - + func browserProfiles(supportDirectoryURL: URL? = nil) -> DataImport.BrowserProfileList? { let applicationSupportURL = supportDirectoryURL ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] - + guard let profilePath = profilesDirectory(applicationSupportURL: applicationSupportURL), let potentialProfileURLs = try? FileManager.default.contentsOfDirectory(at: profilePath, includingPropertiesForKeys: nil, diff --git a/DuckDuckGo/Data Import/View/DataImportViewController.swift b/DuckDuckGo/Data Import/View/DataImportViewController.swift index c2c2481c10..6fe60c9b88 100644 --- a/DuckDuckGo/Data Import/View/DataImportViewController.swift +++ b/DuckDuckGo/Data Import/View/DataImportViewController.swift @@ -412,7 +412,7 @@ final class DataImportViewController: NSViewController { action: error.actionType.pixelEventAction, source: viewState.selectedImportSource.pixelEventSource ) - + Pixel.fire(pixel, withAdditionalParameters: error.errorType.errorParameters) let alert = NSAlert.importFailedAlert(source: viewState.selectedImportSource, linkDelegate: self) @@ -501,20 +501,20 @@ extension DataImportViewController: RequestFilePermissionViewControllerDelegate } extension DataImportViewController: NSTextViewDelegate { - + func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool { guard let sheet = view.window?.attachedSheet else { return false } - + view.window?.endSheet(sheet) dismiss() - + FeedbackPresenter.presentFeedbackForm() - + return true } - + } extension NSPopUpButton { diff --git a/DuckDuckGo/Data Import/View/FileImportViewController.swift b/DuckDuckGo/Data Import/View/FileImportViewController.swift index 5ec912bb1e..5762239c1b 100644 --- a/DuckDuckGo/Data Import/View/FileImportViewController.swift +++ b/DuckDuckGo/Data Import/View/FileImportViewController.swift @@ -138,7 +138,7 @@ final class FileImportViewController: NSViewController { case .selectedValidFile(let fileURL): // In case the import source has changed, the file selection state's info view needs to be refreshed. renderAwaitingFileSelectionState() - + selectedFileContainer.isHidden = false selectedFileLabel.stringValue = fileURL.path if importSource == .bookmarksHTML { diff --git a/DuckDuckGo/Data Import/View/NSAlert+DataImport.swift b/DuckDuckGo/Data Import/View/NSAlert+DataImport.swift index d51f941656..faeb1609e5 100644 --- a/DuckDuckGo/Data Import/View/NSAlert+DataImport.swift +++ b/DuckDuckGo/Data Import/View/NSAlert+DataImport.swift @@ -29,10 +29,10 @@ extension NSAlert { let linkText = UserText.dataImportSubmitFeedback let informativeText = UserText.dataImportFailedBody - + let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 250, height: 0)) textView.applyLabelStyle() - + let attributedString = NSMutableAttributedString(string: informativeText) attributedString.addLink("", toText: linkText) // The actual value of the link isn't important, we're reacting to the click via the delegate attributedString.addAttributes([ @@ -41,10 +41,10 @@ extension NSAlert { ], range: NSRange(location: 0, length: attributedString.length)) textView.textStorage?.setAttributedString(attributedString) - + textView.sizeToFit() textView.delegate = linkDelegate - + alert.messageText = UserText.dataImportFailedTitle alert.accessoryView = textView alert.alertStyle = .warning diff --git a/DuckDuckGo/Device Authentication/DeviceAuthenticationService.swift b/DuckDuckGo/Device Authentication/DeviceAuthenticationService.swift index 0e51b49d99..307a1bc3bd 100644 --- a/DuckDuckGo/Device Authentication/DeviceAuthenticationService.swift +++ b/DuckDuckGo/Device Authentication/DeviceAuthenticationService.swift @@ -21,16 +21,16 @@ import Foundation enum DeviceAuthenticationResult { case success case failure - + var authenticated: Bool { return self == .success } } protocol DeviceAuthenticationService { - + typealias DeviceAuthenticationResultHandler = (DeviceAuthenticationResult) -> Void - + func authenticateDevice(reason: String, result: @escaping DeviceAuthenticationResultHandler) - + } diff --git a/DuckDuckGo/Device Authentication/DeviceIdleStateDetector.swift b/DuckDuckGo/Device Authentication/DeviceIdleStateDetector.swift index 38bfe8da7a..83b147f270 100644 --- a/DuckDuckGo/Device Authentication/DeviceIdleStateDetector.swift +++ b/DuckDuckGo/Device Authentication/DeviceIdleStateDetector.swift @@ -19,7 +19,7 @@ import Foundation protocol DeviceIdleStateProvider { - + func secondsSinceLastEvent() -> TimeInterval - + } diff --git a/DuckDuckGo/Device Authentication/LocalAuthenticationService.swift b/DuckDuckGo/Device Authentication/LocalAuthenticationService.swift index fcdc275701..474d53c750 100644 --- a/DuckDuckGo/Device Authentication/LocalAuthenticationService.swift +++ b/DuckDuckGo/Device Authentication/LocalAuthenticationService.swift @@ -20,7 +20,7 @@ import Foundation import LocalAuthentication final class LocalAuthenticationService: DeviceAuthenticationService { - + func authenticateDevice(reason: String, result: @escaping DeviceAuthenticationResultHandler) { let context = LAContext() @@ -31,5 +31,5 @@ final class LocalAuthenticationService: DeviceAuthenticationService { } } } - + } diff --git a/DuckDuckGo/Device Authentication/QuartzIdleStateProvider.swift b/DuckDuckGo/Device Authentication/QuartzIdleStateProvider.swift index ed7a4df134..19c50f7d4d 100644 --- a/DuckDuckGo/Device Authentication/QuartzIdleStateProvider.swift +++ b/DuckDuckGo/Device Authentication/QuartzIdleStateProvider.swift @@ -21,13 +21,13 @@ import CoreGraphics import os.log final class QuartzIdleStateProvider: DeviceIdleStateProvider { - + func secondsSinceLastEvent() -> TimeInterval { let anyInputEventType = CGEventType(rawValue: ~0)! let seconds = CGEventSource.secondsSinceLastEventType(.hidSystemState, eventType: anyInputEventType) - + os_log("Idle duration since last user input event: %f", log: .autoLock, seconds) - + return seconds } diff --git a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift index c80ace03fe..12965bf078 100644 --- a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift +++ b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift @@ -44,28 +44,28 @@ extension EmailManagerRequestDelegate { }.resume() } // swiftlint:enable function_parameter_count - + public func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) { var parameters = [ "access_type": accessType.rawValue, "error": error.errorDescription ] - + if case let .keychainLookupFailure(status) = error { parameters["keychain_status"] = String(status) parameters["keychain_operation"] = "lookup" } - + if case let .keychainDeleteFailure(status) = error { parameters["keychain_status"] = String(status) parameters["keychain_operation"] = "delete" } - + if case let .keychainSaveFailure(status) = error { parameters["keychain_status"] = String(status) parameters["keychain_operation"] = "save" } - + Pixel.fire(.debug(event: .emailAutofillKeychainError), withAdditionalParameters: parameters) } diff --git a/DuckDuckGo/Email/EmailUrlExtensions.swift b/DuckDuckGo/Email/EmailUrlExtensions.swift index f848647edf..abcb4497bf 100644 --- a/DuckDuckGo/Email/EmailUrlExtensions.swift +++ b/DuckDuckGo/Email/EmailUrlExtensions.swift @@ -24,7 +24,7 @@ extension EmailUrls { private struct Url { static let emailProtectionLink = "https://duckduckgo.com/email" } - + private struct DevUrl { static let emailProtectionLink = "https://quackdev.duckduckgo.com/email" } diff --git a/DuckDuckGo/Favicons/Model/FaviconManager.swift b/DuckDuckGo/Favicons/Model/FaviconManager.swift index 2a6591c0b8..987f75b473 100644 --- a/DuckDuckGo/Favicons/Model/FaviconManager.swift +++ b/DuckDuckGo/Favicons/Model/FaviconManager.swift @@ -27,7 +27,7 @@ protocol FaviconManagement { func loadFavicons() func handleFaviconLinks(_ faviconLinks: [FaviconUserScript.FaviconLink], documentUrl: URL, completion: @escaping (Favicon?) -> Void) - + func handleFavicons(_ favicons: [Favicon], documentUrl: URL) func getCachedFavicon(for documentUrl: URL, sizeCategory: Favicon.SizeCategory) -> Favicon? @@ -45,7 +45,7 @@ final class FaviconManager: FaviconManagement { static let shared = FaviconManager() private lazy var store: FaviconStoring = FaviconStore() - + private let faviconURLSession = URLSession(configuration: .ephemeral) @Published var faviconsLoaded = false @@ -101,7 +101,7 @@ final class FaviconManager: FaviconManagement { return nil } - + let favicon = self.handleFaviconReferenceCacheInsertion(documentURL: documentUrl, cachedFavicons: cachedFavicons, newFavicons: newFavicons) @@ -109,13 +109,13 @@ final class FaviconManager: FaviconManagement { completion(favicon) } } - + func handleFavicons(_ newFavicons: [Favicon], documentUrl: URL) { // Insert new favicons to cache newFavicons.forEach { newFavicon in self.imageCache.insert(newFavicon) } - + let faviconLinks = newFavicons.map(\.url) // Pick most suitable favicons @@ -126,10 +126,10 @@ final class FaviconManager: FaviconManagement { return nil } - + handleFaviconReferenceCacheInsertion(documentURL: documentUrl, cachedFavicons: cachedFavicons, newFavicons: newFavicons) } - + @discardableResult private func handleFaviconReferenceCacheInsertion(documentURL: URL, cachedFavicons: [Favicon], newFavicons: [Favicon]) -> Favicon? { let noFaviconPickedYet = self.referenceCache.getFaviconUrl(for: documentURL, sizeCategory: .small) == nil diff --git a/DuckDuckGo/Favicons/NSNotificationName+Favicons.swift b/DuckDuckGo/Favicons/NSNotificationName+Favicons.swift index 0a514f4cad..af82615c03 100644 --- a/DuckDuckGo/Favicons/NSNotificationName+Favicons.swift +++ b/DuckDuckGo/Favicons/NSNotificationName+Favicons.swift @@ -19,7 +19,7 @@ import Foundation extension NSNotification.Name { - + static let faviconCacheUpdated = NSNotification.Name("FaviconCacheUpdatedNotification") } diff --git a/DuckDuckGo/Feedback and Breakage/View/FeedbackPresenter.swift b/DuckDuckGo/Feedback and Breakage/View/FeedbackPresenter.swift index 0ddcce1258..e7f07a517b 100644 --- a/DuckDuckGo/Feedback and Breakage/View/FeedbackPresenter.swift +++ b/DuckDuckGo/Feedback and Breakage/View/FeedbackPresenter.swift @@ -19,7 +19,7 @@ import Cocoa enum FeedbackPresenter { - + static func presentFeedbackForm() { // swiftlint:disable:next force_cast let windowController = NSStoryboard.feedback.instantiateController(withIdentifier: "FeedbackWindowController") as! NSWindowController diff --git a/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift b/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift index 7cb83f17b0..c9008bd009 100644 --- a/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift +++ b/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift @@ -64,7 +64,7 @@ final class FeedbackViewController: NSViewController { @IBOutlet weak var thankYouView: NSView! private var cancellables = Set() - + private var browserFeedbackConstraint: NSLayoutConstraint? private var browserFeedbackBreakageConstraint: NSLayoutConstraint? @@ -85,10 +85,10 @@ final class FeedbackViewController: NSViewController { super.viewDidLoad() setContentViewHeight(Constants.defaultContentHeight, animated: false) setupTextViews() - + browserFeedbackConstraint = browserFeedbackView.topAnchor.constraint(equalTo: optionPopUpButton.bottomAnchor, constant: 8) browserFeedbackBreakageConstraint = browserFeedbackView.topAnchor.constraint(equalTo: websiteBreakageView.bottomAnchor) - + browserFeedbackConstraint?.isActive = true } @@ -193,7 +193,7 @@ final class FeedbackViewController: NSViewController { pickOptionMenuItem.isEnabled = true return } - + browserFeedbackView.isHidden = false let contentHeight: CGFloat @@ -259,7 +259,7 @@ final class FeedbackViewController: NSViewController { browserFeedbackDescriptionLabel.stringValue = UserText.feedbackOtherDescription } } - + private func updateBrowserFeedbackDisclaimerLabel(for formOption: FormOption) { switch formOption { case .websiteBreakage: diff --git a/DuckDuckGo/File Download/Model/DownloadListItem.swift b/DuckDuckGo/File Download/Model/DownloadListItem.swift index bcb704f701..27f84249ee 100644 --- a/DuckDuckGo/File Download/Model/DownloadListItem.swift +++ b/DuckDuckGo/File Download/Model/DownloadListItem.swift @@ -35,7 +35,7 @@ struct DownloadListItem: Equatable { modified = Date() } } - + var destinationURL: URL? { didSet { guard destinationURL != oldValue else { return } diff --git a/DuckDuckGo/File Download/Model/DownloadViewModel.swift b/DuckDuckGo/File Download/Model/DownloadViewModel.swift index abc3f3886a..d266c6ce3c 100644 --- a/DuckDuckGo/File Download/Model/DownloadViewModel.swift +++ b/DuckDuckGo/File Download/Model/DownloadViewModel.swift @@ -79,7 +79,7 @@ final class DownloadViewModel { } extension DownloadViewModel { - + var error: FileDownloadError? { guard case .failed(let error) = state else { return nil } return error diff --git a/DuckDuckGo/File Download/Model/FileDownloadManager.swift b/DuckDuckGo/File Download/Model/FileDownloadManager.swift index af53c3bca4..ce8b640eea 100644 --- a/DuckDuckGo/File Download/Model/FileDownloadManager.swift +++ b/DuckDuckGo/File Download/Model/FileDownloadManager.swift @@ -182,7 +182,7 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { } locationChooser(suggestedFilename, downloadLocation, fileType.map { [$0] } ?? []) {[weak self] url, fileType in - + if let url = url { self?.preferences.lastUsedCustomDownloadLocation = url.deletingLastPathComponent() @@ -191,7 +191,7 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { try? FileManager.default.removeItem(at: url) } } - + completion(url, fileType) } } diff --git a/DuckDuckGo/File Download/Services/DownloadListCoordinator.swift b/DuckDuckGo/File Download/Services/DownloadListCoordinator.swift index 2b27965b32..b0291cc4dc 100644 --- a/DuckDuckGo/File Download/Services/DownloadListCoordinator.swift +++ b/DuckDuckGo/File Download/Services/DownloadListCoordinator.swift @@ -110,7 +110,7 @@ final class DownloadListCoordinator { } // skip already known task: it's already subscribed guard downloadTaskCancellables[task] == nil else { return } - + let item = item ?? DownloadListItem(task: task) task.$location diff --git a/DuckDuckGo/File Download/Services/DownloadListStore.swift b/DuckDuckGo/File Download/Services/DownloadListStore.swift index 5040632c6d..fdaccce94a 100644 --- a/DuckDuckGo/File Download/Services/DownloadListStore.swift +++ b/DuckDuckGo/File Download/Services/DownloadListStore.swift @@ -142,7 +142,7 @@ final class DownloadListStore: DownloadListStoring { func save(_ item: DownloadListItem, completionHandler: ((Error?) -> Void)?) { guard let context = self.context else { return } - + func mainQueueCompletion(_ error: Error?) { guard completionHandler != nil else { return } DispatchQueue.main.async { diff --git a/DuckDuckGo/File Download/View/DownloadsPopover.swift b/DuckDuckGo/File Download/View/DownloadsPopover.swift index 72c6309d27..aadfac680c 100644 --- a/DuckDuckGo/File Download/View/DownloadsPopover.swift +++ b/DuckDuckGo/File Download/View/DownloadsPopover.swift @@ -19,7 +19,7 @@ import AppKit final class DownloadsPopover: NSPopover { - + override init() { super.init() diff --git a/DuckDuckGo/File Download/View/DownloadsViewController.swift b/DuckDuckGo/File Download/View/DownloadsViewController.swift index 78325163f6..3b7a11157e 100644 --- a/DuckDuckGo/File Download/View/DownloadsViewController.swift +++ b/DuckDuckGo/File Download/View/DownloadsViewController.swift @@ -38,7 +38,7 @@ final class DownloadsViewController: NSViewController { @IBOutlet var openDownloadsFolderButton: NSButton! @IBOutlet var clearDownloadsButton: NSButton! - + @IBOutlet var contextMenu: NSMenu! @IBOutlet var tableView: NSTableView! @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint? @@ -53,7 +53,7 @@ final class DownloadsViewController: NSViewController { super.viewDidLoad() setupDragAndDrop() - + openDownloadsFolderButton.toolTip = UserText.openDownloadsFolderTooltip clearDownloadsButton.toolTip = UserText.clearDownloadHistoryTooltip } diff --git a/DuckDuckGo/Find In Page/FindInPageViewController.swift b/DuckDuckGo/Find In Page/FindInPageViewController.swift index 7fcb7f753f..6c89bf674e 100644 --- a/DuckDuckGo/Find In Page/FindInPageViewController.swift +++ b/DuckDuckGo/Find In Page/FindInPageViewController.swift @@ -32,7 +32,7 @@ final class FindInPageViewController: NSViewController { weak var delegate: FindInPageDelegate? - @Published var model: FindInPageModel? + @Published var model: FindInPageModel? @IBOutlet weak var closeButton: NSButton! @IBOutlet weak var textField: NSTextField! @@ -50,7 +50,7 @@ final class FindInPageViewController: NSViewController { listenForTextFieldResponderNotifications() subscribeToModelChanges() updateFieldStates() - + closeButton.toolTip = UserText.findInPageCloseTooltip nextButton.toolTip = UserText.findInPageNextTooltip previousButton.toolTip = UserText.findInPagePreviousTooltip @@ -122,7 +122,7 @@ final class FindInPageViewController: NSViewController { guard let model = model else { return } statusField.stringValue = String(format: UserText.findInPage, model.currentSelection, model.matchesFound) } - + private func updateView(firstResponder: Bool) { focusRingView.updateView(stroke: firstResponder) } diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 6cdfbed0a9..3552104e2c 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -30,47 +30,47 @@ protocol TabDataClearing { Initiates cleanup of WebKit related data from Tabs: - Detach listeners and observers. - Flush WebView data by navigating to empty page. - + Once done, remove Tab objects. */ final class TabDataCleaner: NSObject, WKNavigationDelegate { - + private var numberOfTabs = 0 private var processedTabs = 0 - + private var completion: (() -> Void)? - + func prepareTabsForCleanup(_ tabs: [TabViewModel], completion: @escaping () -> Void) { guard !tabs.isEmpty else { completion() return } - + assert(self.completion == nil) self.completion = completion - + numberOfTabs = tabs.count tabs.forEach { $0.prepareForDataClearing(caller: self) } } - + private func notifyIfDone() { if processedTabs >= numberOfTabs { completion?() completion = nil } } - + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { processedTabs += 1 - + notifyIfDone() } - + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { Pixel.fire(.debug(event: .blankNavigationOnBurnFailed, error: error)) processedTabs += 1 - + notifyIfDone() } } @@ -81,7 +81,7 @@ final class Fire { static func getBurningDomain(from url: URL) -> String? { return url.host?.droppingWwwPrefix() } - + private typealias TabCollectionsCleanupInfo = [TabCollectionViewModel: [TabCollectionViewModel.TabCleanupInfo]] let webCacheManager: WebCacheManager @@ -94,7 +94,7 @@ final class Fire { let stateRestorationManager: AppStateRestorationManager? let recentlyClosedCoordinator: RecentlyClosedCoordinating? let pinnedTabsManager: PinnedTabsManager - + let tabsCleaner = TabDataCleaner() enum BurningData { @@ -122,7 +122,7 @@ final class Fire { self.faviconManagement = faviconManagement self.recentlyClosedCoordinator = recentlyClosedCoordinator self.pinnedTabsManager = pinnedTabsManager - + if #available(macOS 11, *), autoconsentManagement == nil { self.autoconsentManagement = AutoconsentManagement.shared } else { @@ -157,18 +157,18 @@ final class Fire { }) let burningDomains = domains.union(wwwDomains) let collectionsCleanupInfo = tabViewModelsFor(domains: burningDomains) - + // Prepare all Tabs that are going to be burned let tabsToRemove = collectionsCleanupInfo.values.flatMap { tabViewModelsCleanupInfo in tabViewModelsCleanupInfo.filter({ $0.action == .burn }).compactMap { $0.tabViewModel } } let pinnedTabsViewModels = pinnedTabViewModels(for: burningDomains) - + tabsCleaner.prepareTabsForCleanup(tabsToRemove) { let group = DispatchGroup() - + group.enter() self.burnTabsFrom(collectionViewModels: collectionsCleanupInfo, relatedToDomains: burningDomains) { @@ -216,7 +216,7 @@ final class Fire { func burnAll(tabCollectionViewModel: TabCollectionViewModel, completion: (() -> Void)? = nil) { os_log("Fire started", log: .fire) burningData = .all - + burnLastSessionState() let pinnedTabsViewModels = pinnedTabViewModels() @@ -233,7 +233,7 @@ final class Fire { self.burnPinnedTabs(pinnedTabsViewModels) { group.leave() } - + group.enter() self.burnHistory { self.burnPermissions { @@ -243,7 +243,7 @@ final class Fire { } } } - + group.enter() self.burnWindows(exceptOwnerOf: tabCollectionViewModel) { group.leave() @@ -255,7 +255,7 @@ final class Fire { group.notify(queue: .main) { self.burningData = nil completion?() - + os_log("Fire finished", log: .fire) } } @@ -284,14 +284,14 @@ final class Fire { self.burnDomains(domains, includingHistory: false, completion: completion) } } - + // MARK: - Tabs - + private func allTabViewModels() -> [TabViewModel] { var allTabViewModels = [TabViewModel] () for window in windowControllerManager.mainWindowControllers { let tabCollectionViewModel = window.mainViewController.tabCollectionViewModel - + allTabViewModels.append(contentsOf: tabCollectionViewModel.tabViewModels.values) } return allTabViewModels @@ -404,7 +404,7 @@ final class Fire { completion() } } - + // MARK: - Autoconsent visit cache private func burnAutoconsentCache() { @@ -475,12 +475,12 @@ extension Fire { } fileprivate extension TabCollectionViewModel { - + struct TabCleanupInfo { let tabViewModel: TabViewModel let action: Tab.FireAction } - + // Burns data related to domains from the collection of tabs func clearData(_ cleanupInfo: [TabCleanupInfo], forDomains domains: Set) { // Go one by one and execute the fire action @@ -493,7 +493,7 @@ fileprivate extension TabCollectionViewModel { switch tabCleanupInfo.action { case .none: continue case .replace: - + let tab = Tab(content: tabCleanupInfo.tabViewModel.tab.content, shouldLoadInBackground: true) replaceTab(at: .unpinned(tabIndex), with: tab, forceChange: true) case .burn: diff --git a/DuckDuckGo/Fire/ViewModel/FireViewModel.swift b/DuckDuckGo/Fire/ViewModel/FireViewModel.swift index a816e85953..bc18f777ab 100644 --- a/DuckDuckGo/Fire/ViewModel/FireViewModel.swift +++ b/DuckDuckGo/Fire/ViewModel/FireViewModel.swift @@ -20,7 +20,7 @@ import WebKit import Combine final class FireViewModel { - + let fire: Fire @Published var isAnimationPlaying = false diff --git a/DuckDuckGo/History/Model/HistoryCoordinator.swift b/DuckDuckGo/History/Model/HistoryCoordinator.swift index 2fe24d819e..e5e422a42d 100644 --- a/DuckDuckGo/History/Model/HistoryCoordinator.swift +++ b/DuckDuckGo/History/Model/HistoryCoordinator.swift @@ -243,12 +243,12 @@ final class HistoryCoordinator: HistoryCoordinating { private func removeVisits(_ visits: [Visit], completionHandler: ((Error?) -> Void)? = nil) { var entriesToRemove = [HistoryEntry]() - + // Remove from the local memory visits.forEach { visit in if let historyEntry = visit.historyEntry { historyEntry.visits.remove(visit) - + if historyEntry.visits.count > 0 { if let newLastVisit = historyEntry.visits.map({ $0.date }).max() { historyEntry.lastVisit = newLastVisit @@ -311,7 +311,7 @@ final class HistoryCoordinator: HistoryCoordinating { return } entry.visits.forEach { $0.savingState = .saved } - + historyStoring.save(entry: entryCopy) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in diff --git a/DuckDuckGo/History/Services/HistoryStore.swift b/DuckDuckGo/History/Services/HistoryStore.swift index 3af51313c0..c56235d43f 100644 --- a/DuckDuckGo/History/Services/HistoryStore.swift +++ b/DuckDuckGo/History/Services/HistoryStore.swift @@ -91,7 +91,7 @@ final class HistoryStore: HistoryStoring { let deleteRequest = NSFetchRequest(entityName: HistoryEntryManagedObject.className()) let predicates = identifiers.map({ NSPredicate(format: "identifier == %@", argumentArray: [$0]) }) deleteRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: predicates) - + do { let entriesToDelete = try context.fetch(deleteRequest) for entry in entriesToDelete { @@ -103,7 +103,7 @@ final class HistoryStore: HistoryStoring { return .failure(error) } } - + do { try context.save() } catch { @@ -142,10 +142,10 @@ final class HistoryStore: HistoryStoring { Pixel.fire(.debug(event: .historyCleanEntriesFailed, error: error)) return .failure(error) } - + let visitDeleteRequest = NSFetchRequest(entityName: VisitManagedObject.className()) visitDeleteRequest.predicate = NSPredicate(format: "date < %@", date as NSDate) - + do { let itemsToBeDeleted = try context.fetch(visitDeleteRequest) for item in itemsToBeDeleted { @@ -288,7 +288,7 @@ final class HistoryStore: HistoryStoring { return .failure(error) } } - + do { try context.save() } catch { @@ -392,7 +392,7 @@ private extension Visit { assertionFailure("Bad type or date is nil") return nil } - + self.init(date: date) savingState = .saved } diff --git a/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift b/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift index bd0b17ba75..d9b21e5b57 100644 --- a/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift @@ -61,7 +61,7 @@ extension HomePage.Models { let lastRowCount = favorites.count % HomePage.favoritesPerRow let missing = lastRowCount > 0 ? HomePage.favoritesPerRow - lastRowCount : 0 - (0 ..< missing).forEach { _ in + (0 ..< missing).forEach { _ in favorites.append(FavoriteModel(id: UUID(), favoriteType: .ghostButton)) } @@ -129,5 +129,5 @@ extension HomePage.Models { } } } - + } diff --git a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift index ee93bdce7a..5a5d9f006a 100644 --- a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift @@ -168,7 +168,7 @@ final class RecentlyVisitedSiteModel: ObservableObject { var isRealDomain: Bool { domainPlaceholder == nil } - + private let baseURL: URL? private let domainPlaceholder: String? private let privatePlayer: PrivatePlayer diff --git a/DuckDuckGo/Home Page/View/FavoritesView.swift b/DuckDuckGo/Home Page/View/FavoritesView.swift index 77dd30707b..8a7ebb99fd 100644 --- a/DuckDuckGo/Home Page/View/FavoritesView.swift +++ b/DuckDuckGo/Home Page/View/FavoritesView.swift @@ -53,7 +53,7 @@ struct Favorites: View { } } - + struct FavoritesGrid: View { @EnvironmentObject var model: HomePage.Models.FavoritesModel @@ -207,13 +207,13 @@ struct FavoritesGrid: View { return row } } - + fileprivate struct FavoritesGridAddButton: View { - + @EnvironmentObject var model: HomePage.Models.FavoritesModel var body: some View { - + ZStack(alignment: .top) { FavoriteTemplate(title: UserText.addFavorite, domain: nil) ZStack { @@ -225,15 +225,15 @@ fileprivate struct FavoritesGridAddButton: View { .link { model.addNew() } - + } - + } - + fileprivate struct FavoritesGridGhostButton: View { - + var body: some View { - + VStack { RoundedRectangle(cornerRadius: 12) .stroke(Color("HomeFavoritesGhostColor"), style: StrokeStyle(lineWidth: 1.5, dash: [4.0, 2.0])) @@ -241,9 +241,9 @@ fileprivate struct FavoritesGridGhostButton: View { Spacer() } .frame(width: FavoritesGrid.GridDimensions.itemWidth, height: FavoritesGrid.GridDimensions.itemHeight) - + } - + } struct FavoriteTemplate: View { @@ -281,7 +281,7 @@ struct FavoriteTemplate: View { .frame(maxWidth: FavoritesGrid.GridDimensions.itemWidth) .onHover { isHovering in self.isHovering = isHovering - + if isHovering { NSCursor.pointingHand.push() } else { diff --git a/DuckDuckGo/Main/View/MainViewController.swift b/DuckDuckGo/Main/View/MainViewController.swift index b855f50699..647225e652 100644 --- a/DuckDuckGo/Main/View/MainViewController.swift +++ b/DuckDuckGo/Main/View/MainViewController.swift @@ -56,7 +56,7 @@ final class MainViewController: NSViewController { private var bookmarksBarIsVisible: Bool { return bookmarksBarViewController.parent != nil } - + private var isInPopUpWindow: Bool { view.window?.isPopUpWindow == true } @@ -92,11 +92,11 @@ final class MainViewController: NSViewController { navigationBarContainerView.layer?.masksToBounds = false resizeNavigationBarForHomePage(tabCollectionViewModel.selectedTabViewModel?.tab.content == .homePage, animated: false) - + let bookmarksBarVisible = PersistentAppInterfaceSettings.shared.showBookmarksBar updateBookmarksBarViewVisibility(visible: bookmarksBarVisible) } - + updateDividerColor() } @@ -180,14 +180,14 @@ final class MainViewController: NSViewController { self.fireViewController = fireViewController return fireViewController } - + @IBSegueAction func createBookmarksBar(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> BookmarksBarViewController? { let bookmarksBarViewController = BookmarksBarViewController(coder: coder, tabCollectionViewModel: tabCollectionViewModel) self.bookmarksBarViewController = bookmarksBarViewController return bookmarksBarViewController } - + private func updateBookmarksBarViewVisibility(visible: Bool) { let showBookmarksBar = isInPopUpWindow ? false : visible @@ -202,12 +202,12 @@ final class MainViewController: NSViewController { bookmarksBarViewController.removeFromParent() bookmarksBarViewController.view.removeFromSuperview() } - + bookmarksBarHeightConstraint.constant = showBookmarksBar ? 34 : 0 updateDividerColor() } - + private func updateDividerColor() { NSAppearance.withAppAppearance { let isHomePage = tabCollectionViewModel.selectedTabViewModel?.tab.content == .homePage @@ -226,7 +226,7 @@ final class MainViewController: NSViewController { self?.subscribeToTitleChange() } } - + private func subscribeToTitleChange() { guard let window = self.view.window else { return } windowTitleCancellable = tabCollectionViewModel.$selectedTabViewModel diff --git a/DuckDuckGo/Main/View/MainWindow.swift b/DuckDuckGo/Main/View/MainWindow.swift index 88e13e67ab..96c488969c 100644 --- a/DuckDuckGo/Main/View/MainWindow.swift +++ b/DuckDuckGo/Main/View/MainWindow.swift @@ -63,7 +63,7 @@ final class MainWindow: NSWindow { // Send it after the first responder has been set on the super class so that window.firstResponder matches correctly postFirstResponderNotification(with: responder) } - + return super.makeFirstResponder(responder) } diff --git a/DuckDuckGo/Main/View/MainWindowController.swift b/DuckDuckGo/Main/View/MainWindowController.swift index 98b7396b89..4991d8e886 100644 --- a/DuckDuckGo/Main/View/MainWindowController.swift +++ b/DuckDuckGo/Main/View/MainWindowController.swift @@ -73,12 +73,12 @@ final class MainWindowController: NSWindowController { private func setupWindow() { window?.delegate = self window?.setFrameAutosaveName(Self.windowFrameSaveName) - + if shouldShowOnboarding { mainViewController.tabCollectionViewModel.selectedTabViewModel?.tab.startOnboarding() } } - + private func subscribeToResolutionChange() { NotificationCenter.default.addObserver(forName: NSApplication.didChangeScreenParametersNotification, object: NSApplication.shared, @@ -86,11 +86,11 @@ final class MainWindowController: NSWindowController { self?.resizeWindowIfNeeded() } } - + private func resizeWindowIfNeeded() { if let visibleWindowFrame = window?.screen?.visibleFrame, let windowFrame = window?.frame { - + if windowFrame.width > visibleWindowFrame.width || windowFrame.height > visibleWindowFrame.height { window?.performZoom(nil) } @@ -136,7 +136,7 @@ final class MainWindowController: NSWindowController { mainViewController.tabBarViewController.fireButton.isEnabled = !prevented mainViewController.navigationBarViewController.controlsForUserPrevention.forEach { $0?.isEnabled = !prevented } - + NSApplication.shared.mainMenuTyped.autoupdatingMenusForUserPrevention.forEach { $0.autoenablesItems = !prevented } NSApplication.shared.mainMenuTyped.menuItemsForUserPrevention.forEach { $0.isEnabled = !prevented } @@ -244,7 +244,7 @@ fileprivate extension MainMenu { preferencesMenuItem ] } - + var autoupdatingMenusForUserPrevention: [NSMenu] { return [ preferencesMenuItem.menu, diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index a598773dd3..be14287ba0 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -67,7 +67,7 @@ final class MainMenu: NSMenu { @IBOutlet weak var bookmarkThisPageMenuItem: NSMenuItem? @IBOutlet weak var favoritesMenuItem: NSMenuItem? @IBOutlet weak var favoriteThisPageMenuItem: NSMenuItem? - + @IBOutlet weak var toggleBookmarksBarMenuItem: NSMenuItem? @IBOutlet weak var toggleAutofillShortcutMenuItem: NSMenuItem? @IBOutlet weak var toggleBookmarksShortcutMenuItem: NSMenuItem? @@ -240,7 +240,7 @@ final class MainMenu: NSMenu { toggleBookmarksBarMenuItem?.title = title bookmarksMenuToggleBookmarksBarMenuItem?.title = title } - + private func updateShortcutMenuItems() { toggleAutofillShortcutMenuItem?.title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .autofill) toggleBookmarksShortcutMenuItem?.title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .bookmarks) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 0e62f19151..3a4c68665b 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -313,15 +313,15 @@ extension MainViewController { @IBAction func toggleBookmarksBar(_ sender: Any) { PersistentAppInterfaceSettings.shared.showBookmarksBar.toggle() } - + @IBAction func toggleAutofillShortcut(_ sender: Any) { LocalPinningManager.shared.togglePinning(for: .autofill) } - + @IBAction func toggleBookmarksShortcut(_ sender: Any) { LocalPinningManager.shared.togglePinning(for: .bookmarks) } - + @IBAction func toggleDownloadsShortcut(_ sender: Any) { LocalPinningManager.shared.togglePinning(for: .downloads) } @@ -625,13 +625,13 @@ extension MainViewController { NotificationCenter.default.post(name: .ShowSaveCredentialsPopover, object: nil) #endif } - + @IBAction func showCredentialsSavedPopover(_ sender: Any?) { #if DEBUG || REVIEW NotificationCenter.default.post(name: .ShowCredentialsSavedPopover, object: nil) #endif } - + @IBAction func fetchConfigurationNow(_ sender: Any?) { ConfigurationManager.shared.lastUpdateTime = .distantPast ConfigurationManager.shared.refreshIfNeeded() diff --git a/DuckDuckGo/Navigation Bar/PinningManager.swift b/DuckDuckGo/Navigation Bar/PinningManager.swift index 5a4ac6bf46..6ebf64c7b6 100644 --- a/DuckDuckGo/Navigation Bar/PinningManager.swift +++ b/DuckDuckGo/Navigation Bar/PinningManager.swift @@ -25,18 +25,18 @@ enum PinnableView: String { } protocol PinningManager { - + func togglePinning(for view: PinnableView) func isPinned(_ view: PinnableView) -> Bool - + } final class LocalPinningManager: PinningManager { static let shared = LocalPinningManager() - + static let pinnedViewChangedNotificationViewTypeKey = "pinning.pinnedViewChanged.viewType" - + @UserDefaultsWrapper(key: .pinnedViews, defaultValue: []) private var pinnedViewStrings: [String] @@ -46,16 +46,16 @@ final class LocalPinningManager: PinningManager { } else { pinnedViewStrings.append(view.rawValue) } - + NotificationCenter.default.post(name: .PinnedViewsChanged, object: nil, userInfo: [ Self.pinnedViewChangedNotificationViewTypeKey: view.rawValue ]) } - + func isPinned(_ view: PinnableView) -> Bool { return pinnedViewStrings.contains(view.rawValue) } - + func toggleShortcutInterfaceTitle(for view: PinnableView) -> String { switch view { case .autofill: return isPinned(.autofill) ? UserText.hideAutofillShortcut : UserText.showAutofillShortcut diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift index a2c9e457c1..235595846f 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift @@ -95,7 +95,7 @@ final class AddressBarButtonsViewController: NSViewController { var shieldAnimationView: AnimationView! var shieldDotAnimationView: AnimationView! @IBOutlet weak var notificationAnimationView: NavigationBarBadgeAnimationView! - + @IBOutlet weak var permissionButtons: NSView! @IBOutlet weak var cameraButton: PermissionButton! { didSet { @@ -169,9 +169,9 @@ final class AddressBarButtonsViewController: NSViewController { private var trackerAnimationTriggerCancellable: AnyCancellable? private var isMouseOverAnimationVisibleCancellable: AnyCancellable? private var privacyInfoCancellable: AnyCancellable? - + private lazy var buttonsBadgeAnimator = NavigationBarBadgeAnimator() - + required init?(coder: NSCoder) { fatalError("AddressBarButtonsViewController: Bad initializer") } @@ -193,18 +193,18 @@ final class AddressBarButtonsViewController: NSViewController { subscribeToEffectiveAppearance() subscribeToIsMouseOverAnimationVisible() updateBookmarkButtonVisibility() - + privacyEntryPointButton.toolTip = UserText.privacyDashboardTooltip } override func viewWillAppear() { setupButtons() } - + override func viewDidAppear() { super.viewDidAppear() } - + func showBadgeNotification(_ type: NavigationBarBadgeAnimationView.AnimationType) { if !isAnyShieldAnimationPlaying { buttonsBadgeAnimator.showNotification(withType: .cookieManaged, @@ -215,7 +215,7 @@ final class AddressBarButtonsViewController: NSViewController { animationType: type) } } - + private func playBadgeAnimationIfNecessary() { if let queuedNotification = buttonsBadgeAnimator.queuedAnimation { // Add small time gap in between animations if badge animation was queued @@ -255,7 +255,7 @@ final class AddressBarButtonsViewController: NSViewController { @IBAction func clearButtonAction(_ sender: Any) { delegate?.addressBarButtonsViewControllerClearButtonClicked(self) } - + @IBAction func privacyEntryPointButtonAction(_ sender: Any) { if let permissionAuthorizationPopover, permissionAuthorizationPopover.isShown { permissionAuthorizationPopover.close() @@ -344,15 +344,15 @@ final class AddressBarButtonsViewController: NSViewController { privacyDashboardPopover.close() return } - + privacyDashboardPopover.viewController.updateTabViewModel(selectedTabViewModel) - + let positioningViewInWindow = privacyDashboardPositioningView.convert(privacyDashboardPositioningView.bounds, to: view.window?.contentView) privacyDashboardPopover.setPreferredMaxHeight(positioningViewInWindow.origin.y) privacyDashboardPopover.show(relativeTo: privacyDashboardPositioningView.bounds, of: privacyDashboardPositioningView, preferredEdge: .maxY) privacyEntryPointButton.state = .on - + privacyInfoCancellable?.cancel() privacyInfoCancellable = selectedTabViewModel.tab.$privacyInfo .dropFirst() @@ -475,7 +475,7 @@ final class AddressBarButtonsViewController: NSViewController { } permissions = [(permissionType, state)] - + PermissionContextMenu(permissions: permissions, domain: selectedTabViewModel.tab.content.url?.host ?? "", delegate: self) @@ -484,7 +484,7 @@ final class AddressBarButtonsViewController: NSViewController { private func setupButtons() { if view.window?.isPopUpWindow == true { - privacyEntryPointButton.position = .free + privacyEntryPointButton.position = .free cameraButton.position = .free geolocationButton.position = .free popupsButton.position = .free @@ -523,7 +523,7 @@ final class AddressBarButtonsViewController: NSViewController { animationViewCache[animationName] = animationView return animationView } - + private func setupNotificationAnimationView() { notificationAnimationView.alphaValue = 0.0 } @@ -646,7 +646,7 @@ final class AddressBarButtonsViewController: NSViewController { } guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { return } - + if controllerMode == .editing(isUrl: false) { [geolocationButton, cameraButton, microphoneButton, popupsButton, externalSchemeButton].forEach { $0?.buttonState = .none @@ -841,7 +841,7 @@ final class AddressBarButtonsViewController: NSViewController { private func closePopover() { privacyDashboardPopover?.close() } - + private func stopAnimations(trackerAnimations: Bool = true, shieldAnimations: Bool = true, badgeAnimations: Bool = true) { @@ -865,7 +865,7 @@ final class AddressBarButtonsViewController: NSViewController { stopNotificationBadgeAnimations() } } - + private func stopNotificationBadgeAnimations() { notificationAnimationView.removeAnimation() buttonsBadgeAnimator.queuedAnimation = nil @@ -926,7 +926,7 @@ final class AddressBarButtonsViewController: NSViewController { isMouseOverAnimationVisibleCancellable = privacyEntryPointButton.$isAnimationViewVisible .dropFirst() .sink { [weak self] isAnimationViewVisible in - + if isAnimationViewVisible { self?.stopAnimations(trackerAnimations: false, shieldAnimations: true, badgeAnimations: false) } else { diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift index 582cf704b1..0507aad991 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift @@ -340,7 +340,7 @@ final class AddressBarTextField: NSTextField { upgradeToHttps(url: url, completion: completion) } - + private func upgradeToHttps(url: URL, completion: @escaping (URL?, Bool) -> Void) { Task { let result = await PrivacyFeatures.httpsUpgrade.upgrade(url: url) @@ -860,7 +860,7 @@ extension AddressBarTextField: NSTextViewDelegate { let pasteAndDoMenuItem = makePasteAndDoMenuItem() { textViewMenu.insertItem(pasteAndDoMenuItem, at: pasteMenuItemIndex + 1) } - + if let insertionPoint = menuItemInsertionPoint(within: menu) { additionalMenuItems.reversed().forEach { item in textViewMenu.insertItem(item, at: insertionPoint) @@ -870,10 +870,10 @@ extension AddressBarTextField: NSTextViewDelegate { textViewMenu.addItem(item) } } - + return textViewMenu } - + /// Returns the menu item after which new items should be added. /// This will be the first separator that comes after a predefined list of items: Cut, Copy, or Paste. /// @@ -881,13 +881,13 @@ extension AddressBarTextField: NSTextViewDelegate { private func menuItemInsertionPoint(within menu: NSMenu) -> Int? { let preferredSelectorNames = ["cut:", "copy:", "paste:"] var foundPreferredSelector = false - + for (index, item) in menu.items.enumerated() { if foundPreferredSelector && item.isSeparatorItem { let indexAfterSeparator = index + 1 return menu.items.indices.contains(indexAfterSeparator) ? indexAfterSeparator : index } - + if let action = item.action, preferredSelectorNames.contains(action.description) { foundPreferredSelector = true } @@ -945,7 +945,7 @@ extension AddressBarTextField: NSTextViewDelegate { return menuItem } - + private func makeFullWebsiteAddressMenuItem() -> NSMenuItem { let menuItem = NSMenuItem( title: UserText.showFullWebsiteAddress.localizedCapitalized, diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeAnimationView.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeAnimationView.swift index fd461ff363..4964ee1c3e 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeAnimationView.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeAnimationView.swift @@ -23,13 +23,13 @@ struct BadgeAnimationView: View { let iconView: AnyView let text: String @State var textOffset: CGFloat = -Consts.View.textScrollerOffset - + var body: some View { GeometryReader { geometry in ZStack { ExpandableRectangle(animationModel: animationModel) .frame(width: geometry.size.width, height: geometry.size.height) - + HStack { Text(text) .foregroundColor(.primary) @@ -50,10 +50,10 @@ struct BadgeAnimationView: View { } }) .padding(.leading, geometry.size.height) - + Spacer() }.clipped() - + // Opaque view HStack { Rectangle() @@ -62,7 +62,7 @@ struct BadgeAnimationView: View { .frame(width: geometry.size.height - Consts.View.opaqueViewOffset, height: geometry.size.height) Spacer() } - + HStack { iconView .frame(width: geometry.size.height, height: geometry.size.height) @@ -89,7 +89,7 @@ struct ExpandableRectangle: View { withAnimation(.easeInOut(duration: animationModel.duration)) { width = geometry.size.width - geometry.size.height } - + case .retracted: withAnimation(.easeInOut(duration: animationModel.duration)) { width = 0 @@ -121,7 +121,7 @@ private enum Consts { static let opaqueViewOffset: CGFloat = 8 static let textScrollerOffset: CGFloat = 120 } - + enum Colors { static let badgeBackgroundColor = Color("URLNotificationBadgeBackground") } diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeNotificationAnimationModel.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeNotificationAnimationModel.swift index af6d09a6c5..0ee9c0d996 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeNotificationAnimationModel.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/BadgeAnimationContainer/BadgeNotificationAnimationModel.swift @@ -27,7 +27,7 @@ final class BadgeNotificationAnimationModel: ObservableObject { self.duration = duration self.secondPhaseDelay = secondPhaseDelay } - + enum AnimationState { case unstarted case expanded diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationContainerView.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationContainerView.swift index f0f714969a..c8d005f01c 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationContainerView.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationContainerView.swift @@ -22,31 +22,31 @@ import SwiftUI final class CookieManagedNotificationContainerView: NSView, NotificationBarViewAnimated { private let cookieAnimationModel = CookieNotificationAnimationModel() private let badgeAnimationModel = BadgeNotificationAnimationModel() - + private lazy var hostingView: NSHostingView = { let view = NSHostingView(rootView: CookieManagedNotificationView(animationModel: cookieAnimationModel, badgeAnimationModel: badgeAnimationModel)) view.frame = bounds return view }() - + override init(frame frameRect: NSRect) { super.init(frame: frameRect) setupView() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupView() { addSubview(hostingView) setupConstraints() } - + private func setupConstraints() { hostingView.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ hostingView.leadingAnchor.constraint(equalTo: leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: trailingAnchor), @@ -54,8 +54,8 @@ final class CookieManagedNotificationContainerView: NSView, NotificationBarViewA hostingView.topAnchor.constraint(equalTo: topAnchor) ]) } - - func startAnimation(_ completion: @escaping () -> Void) { + + func startAnimation(_ completion: @escaping () -> Void) { let totalDuration = (badgeAnimationModel.duration * 2) + badgeAnimationModel.secondPhaseDelay self.startCookieAnimation() @@ -65,14 +65,14 @@ final class CookieManagedNotificationContainerView: NSView, NotificationBarViewA completion() } } - + private func startBadgeAnimation() { badgeAnimationModel.state = .expanded DispatchQueue.main.asyncAfter(deadline: .now() + badgeAnimationModel.secondPhaseDelay) { self.badgeAnimationModel.state = .retracted } } - + private func startCookieAnimation() { cookieAnimationModel.state = .firstPhase DispatchQueue.main.asyncAfter(deadline: .now() + cookieAnimationModel.secondPhaseDelay) { diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationView.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationView.swift index 484c2bdb56..50b948f326 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationView.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieManagedNotificationView.swift @@ -21,7 +21,7 @@ import SwiftUI struct CookieManagedNotificationView: View { @ObservedObject var animationModel: CookieNotificationAnimationModel var badgeAnimationModel: BadgeNotificationAnimationModel - + var body: some View { BadgeAnimationView(animationModel: badgeAnimationModel, iconView: AnyView(CookieAnimationView(animationModel: animationModel)), @@ -31,10 +31,10 @@ struct CookieManagedNotificationView: View { struct CookieAnimationView: View { @ObservedObject var animationModel: CookieNotificationAnimationModel - + @State private var cookieAlpha: CGFloat = 1 @State private var bittenCookieAlpha: CGFloat = 0 - + var body: some View { Group { ZStack(alignment: .center) { @@ -43,23 +43,23 @@ struct CookieAnimationView: View { .resizable() .foregroundColor(.primary) .opacity(cookieAlpha) - + Image("CookieBite") .resizable() .foregroundColor(.primary) .opacity(bittenCookieAlpha) - + InnerExpandingCircle(animationModel: animationModel) OuterExpandingCircle(animationModel: animationModel) } .frame(width: Consts.Layout.cookieSize, height: Consts.Layout.cookieSize) - + DotGroupView(animationModel: animationModel, circleCount: Consts.Count.circle) .frame(width: Consts.Layout.dotsGroupSize, height: Consts.Layout.dotsGroupSize) - + } }.frame(width: Consts.Layout.dotsGroupSize * 1.6, height: Consts.Layout.dotsGroupSize * 1.6) @@ -80,15 +80,15 @@ struct CookieAnimationView: View { private struct DotGroupView: View { var animationModel: CookieNotificationAnimationModel let circleCount: Int - + private func degreesOffset(for index: Int) -> Double { return Double(((360 / circleCount) * index) + Int.random(in: 0.. CGFloat { return isContracted ? proxy.size.width/2 : size/2 + expandedOffset } - + private func yPositionWithGeometry(_ proxy: GeometryProxy, isContracted: Bool) -> CGFloat { return isContracted ? proxy.size.height/2 : size/2 + expandedOffset } @@ -212,19 +212,19 @@ struct CookieManagedNotificationView_Previews: PreviewProvider { } private enum Consts { - + enum Colors { static let badgeBackgroundColor = Color("URLNotificationBadgeBackground") } - + enum CookieAnimation { static let innerExpandingCircleScale1 = 1.0 static let innerExpandingCircleScale2 = 1.4 - + static let outerExpandingCircleScale1 = 1.2 static let outerExpandingCircleScale2 = 1.8 } - + enum BadgeAnimation { static let duration: CGFloat = 0.8 static let secondPhaseDelay = 3.0 @@ -237,7 +237,7 @@ private enum Consts { static let dotSize: CGFloat = 3 static let cornerRadius: CGFloat = 5 } - + enum Count { static let circle = 5 } diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieNotificationAnimationModel.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieNotificationAnimationModel.swift index c6de72ee04..fb63f096c6 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieNotificationAnimationModel.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/CookieManaged/CookieNotificationAnimationModel.swift @@ -24,13 +24,13 @@ final class CookieNotificationAnimationModel: ObservableObject { case firstPhase case secondPhase } - + @Published var state: AnimationState = .unstarted - + let duration: CGFloat let secondPhaseDelay: CGFloat let halfDuration: CGFloat - + init(duration: CGFloat = AnimationDefaultConsts.totalDuration) { self.duration = duration self.halfDuration = duration / 2.0 diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimationView.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimationView.swift index c555258aa0..dce836ab4a 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimationView.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimationView.swift @@ -24,7 +24,7 @@ protocol NotificationBarViewAnimated: NSView { final class NavigationBarBadgeAnimationView: NSView { var animatedView: NotificationBarViewAnimated? - + enum AnimationType { case cookieManaged } @@ -36,27 +36,27 @@ final class NavigationBarBadgeAnimationView: NSView { case .cookieManaged: viewToAnimate = CookieManagedNotificationContainerView() } - + addSubview(viewToAnimate) animatedView = viewToAnimate setupConstraints() } - + func startAnimation(completion: @escaping () -> Void) { self.animatedView?.startAnimation(completion) } - + func removeAnimation() { animatedView?.removeFromSuperview() } - + private func setupConstraints() { guard let animatedView = animatedView else { return } animatedView.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ animatedView.leadingAnchor.constraint(equalTo: leadingAnchor), animatedView.trailingAnchor.constraint(equalTo: trailingAnchor), diff --git a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimator.swift b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimator.swift index d97e187c00..e414fde5ef 100644 --- a/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimator.swift +++ b/DuckDuckGo/Navigation Bar/View/Animations/BadgeAnimations/NavigationBarBadgeAnimator.swift @@ -22,12 +22,12 @@ final class NavigationBarBadgeAnimator: NSObject { var queuedAnimation: QueueData? private var animationID: UUID? private(set) var isAnimating = false - + struct QueueData { var selectedTab: Tab? var animationType: NavigationBarBadgeAnimationView.AnimationType } - + private enum ButtonsFade { case start case end @@ -37,18 +37,18 @@ final class NavigationBarBadgeAnimator: NSObject { buttonsContainer: NSView, and notificationBadgeContainer: NavigationBarBadgeAnimationView) { queuedAnimation = nil - + isAnimating = true let newAnimationID = UUID() self.animationID = newAnimationID - + notificationBadgeContainer.prepareAnimation(.cookieManaged) - + animateButtonsFade(.start, buttonsContainer: buttonsContainer, notificationBadgeContainer: notificationBadgeContainer) { - + notificationBadgeContainer.startAnimation { [weak self] in if self?.animationID == newAnimationID { self?.animateButtonsFade(.end, @@ -60,12 +60,12 @@ final class NavigationBarBadgeAnimator: NSObject { } } } - + private func animateButtonsFade(_ fadeType: ButtonsFade, buttonsContainer: NSView, notificationBadgeContainer: NavigationBarBadgeAnimationView, completionHandler: @escaping (() -> Void)) { - + let animationDuration: CGFloat = 0.25 NSAnimationContext.runAnimationGroup { context in diff --git a/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift b/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift index 82f8dbb37d..e7df3e62b9 100644 --- a/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift @@ -22,7 +22,7 @@ import WebKit import BrowserServicesKit protocol OptionsButtonMenuDelegate: AnyObject { - + func optionsButtonMenuRequestedBookmarkThisPage(_ sender: NSMenuItem) func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) func optionsButtonMenuRequestedToggleBookmarksBar(_ menu: NSMenu) @@ -42,22 +42,22 @@ final class MoreOptionsMenu: NSMenu { private let tabCollectionViewModel: TabCollectionViewModel private let emailManager: EmailManager private let passwordManagerCoordinator: PasswordManagerCoordinating - + required init(coder: NSCoder) { fatalError("MoreOptionsMenu: Bad initializer") } - + init(tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), passwordManagerCoordinator: PasswordManagerCoordinator) { - + self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager self.passwordManagerCoordinator = passwordManagerCoordinator super.init(title: "") - + self.emailManager.requestDelegate = self - + setupMenuItems() } @@ -116,19 +116,19 @@ final class MoreOptionsMenu: NSMenu { @objc func bookmarkPage(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkThisPage(sender) } - + @objc func openBookmarks(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkPopover(self) } - + @objc func openBookmarksManagementInterface(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkManagementInterface(self) } - + @objc func toggleBookmarksBar(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedToggleBookmarksBar(self) } - + @objc func openBookmarkImportInterface(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkImportInterface(self) } @@ -144,7 +144,7 @@ final class MoreOptionsMenu: NSMenu { @objc func openAutofillWithLogins(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedLoginsPopover(self, selectedCategory: .logins) } - + @objc func openExternalPasswordManager(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedOpenExternalPasswordManager(self) } @@ -187,7 +187,7 @@ final class MoreOptionsMenu: NSMenu { private func addUtilityItems() { let bookmarksSubMenu = BookmarksSubMenu(targetting: self, tabCollectionViewModel: tabCollectionViewModel) - + addItem(withTitle: UserText.bookmarks, action: #selector(openBookmarks), keyEquivalent: "") .targetting(self) .withImage(NSImage(named: "Bookmarks")) @@ -352,7 +352,7 @@ final class ZoomSubMenu: NSMenu { } final class BookmarksSubMenu: NSMenu { - + init(targetting target: AnyObject, tabCollectionViewModel: TabCollectionViewModel) { super.init(title: UserText.passwordManagement) self.autoenablesItems = false @@ -369,42 +369,42 @@ final class BookmarksSubMenu: NSMenu { .targetting(target) bookmarkPageItem.isEnabled = tabCollectionViewModel.selectedTabViewModel?.canBeBookmarked == true - + addItem(NSMenuItem.separator()) - + addItem(withTitle: UserText.bookmarksShowToolbarPanel, action: #selector(MoreOptionsMenu.openBookmarks(_:)), keyEquivalent: "") .targetting(target) addItem(NSMenuItem.separator()) - + if let favorites = LocalBookmarkManager.shared.list?.favoriteBookmarks { let favoriteViewModels = favorites.compactMap(BookmarkViewModel.init(entity:)) let potentialItems = bookmarkMenuItems(from: favoriteViewModels) - + let favoriteMenuItems = potentialItems.isEmpty ? [NSMenuItem.empty] : potentialItems - + let favoritesItem = addItem(withTitle: UserText.favorites, action: nil, keyEquivalent: "") favoritesItem.submenu = NSMenu(items: favoriteMenuItems) favoritesItem.image = NSImage(named: "Favorite") - + addItem(NSMenuItem.separator()) } - + guard let entities = LocalBookmarkManager.shared.list?.topLevelEntities else { return } - + let bookmarkViewModels = entities.compactMap(BookmarkViewModel.init(entity:)) let menuItems = bookmarkMenuItems(from: bookmarkViewModels, topLevel: true) - + self.items.append(contentsOf: menuItems) - + addItem(NSMenuItem.separator()) addItem(withTitle: UserText.importBrowserData, action: #selector(MoreOptionsMenu.openBookmarkImportInterface(_:)), keyEquivalent: "") .targetting(target) } - + private func bookmarkMenuItems(from bookmarkViewModels: [BookmarkViewModel], topLevel: Bool = true) -> [NSMenuItem] { var menuItems = [NSMenuItem]() @@ -435,7 +435,7 @@ final class BookmarksSubMenu: NSMenu { return menuItems } - + } final class LoginsSubMenu: NSMenu { @@ -459,7 +459,7 @@ final class LoginsSubMenu: NSMenu { let autofillSelector: Selector let autofillTitle: String - + if passwordManagerCoordinator.isEnabled { autofillSelector = #selector(MoreOptionsMenu.openExternalPasswordManager) autofillTitle = "\(UserText.passwordManagementLogins) (\(UserText.openIn(value: passwordManagerCoordinator.displayName)))" @@ -467,7 +467,7 @@ final class LoginsSubMenu: NSMenu { autofillSelector = #selector(MoreOptionsMenu.openAutofillWithLogins) autofillTitle = UserText.passwordManagementLogins } - + addItem(withTitle: autofillTitle, action: autofillSelector, keyEquivalent: "") .targetting(target) .withImage(NSImage(named: "LoginGlyph")) @@ -502,7 +502,7 @@ extension NSMenuItem { self.submenu = submenu return self } - + @discardableResult func withModifierMask(_ mask: NSEvent.ModifierFlags) -> NSMenuItem { self.keyEquivalentModifierMask = mask diff --git a/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift b/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift index 30b4c7574d..c958e1b691 100644 --- a/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift @@ -112,9 +112,9 @@ final class NavigationBarViewController: NSViewController { optionsButton.sendAction(on: .leftMouseDown) bookmarkListButton.sendAction(on: .leftMouseDown) downloadsButton.sendAction(on: .leftMouseDown) - + optionsButton.toolTip = UserText.applicationMenuTooltip - + #if DEBUG || REVIEW addDebugNotificationListeners() #endif @@ -199,7 +199,7 @@ final class NavigationBarViewController: NSViewController { } @IBAction func optionsButtonAction(_ sender: NSButton) { - + let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: PasswordManagerCoordinator.shared) menu.actionDelegate = self @@ -225,22 +225,22 @@ final class NavigationBarViewController: NSViewController { NSMenu.popUpContextMenu(menu, with: event, for: view) return } - + super.mouseDown(with: event) } - + func listenToPasswordManagerNotifications() { passwordManagerNotificationCancellable = NotificationCenter.default.publisher(for: .PasswordManagerChanged).sink { [weak self] _ in self?.updatePasswordManagementButton() } } - + func listenToPinningManagerNotifications() { pinnedViewsNotificationCancellable = NotificationCenter.default.publisher(for: .PinnedViewsChanged).sink { [weak self] notification in guard let self = self else { return } - + if let userInfo = notification.userInfo as? [String: Any], let viewType = userInfo[LocalPinningManager.pinnedViewChangedNotificationViewTypeKey] as? String, let view = PinnableView(rawValue: viewType) { @@ -309,7 +309,7 @@ final class NavigationBarViewController: NSViewController { // if the tab is not active, don't show the popup return } - self.addressBarViewController?.addressBarButtonsViewController?.showBadgeNotification(.cookieManaged) + self.addressBarViewController?.addressBarButtonsViewController?.showBadgeNotification(.cookieManaged) } } } @@ -331,7 +331,7 @@ final class NavigationBarViewController: NSViewController { let forwardButtonMenu = NSMenu() forwardButtonMenu.delegate = goForwardButtonMenuDelegate goForwardButton.menu = forwardButtonMenu - + goBackButton.toolTip = UserText.navigateBackTooltip goForwardButton.toolTip = UserText.navigateForwardTooltip refreshButton.toolTip = UserText.refreshPageTooltip @@ -414,7 +414,7 @@ final class NavigationBarViewController: NSViewController { .assign(to: \.progress, onWeaklyHeld: downloadsProgressView) .store(in: &downloadsCancellables) } - + private func addContextMenu() { let menu = NSMenu() menu.delegate = self @@ -425,10 +425,10 @@ final class NavigationBarViewController: NSViewController { let menu = NSMenu() let title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .autofill) menu.addItem(withTitle: title, action: #selector(toggleAutofillPanelPinning), keyEquivalent: "") - + passwordManagementButton.menu = menu passwordManagementButton.toolTip = UserText.autofillShortcutTooltip - + let url = tabCollectionViewModel.selectedTabViewModel?.tab.content.url passwordManagementButton.image = NSImage(named: "PasswordManagement") @@ -459,10 +459,10 @@ final class NavigationBarViewController: NSViewController { let menu = NSMenu() let title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .downloads) menu.addItem(withTitle: title, action: #selector(toggleDownloadsPanelPinning(_:)), keyEquivalent: "") - + downloadsButton.menu = menu downloadsButton.toolTip = UserText.downloadsShortcutTooltip - + if LocalPinningManager.shared.isPinned(.downloads) { downloadsButton.isHidden = false return @@ -476,7 +476,7 @@ final class NavigationBarViewController: NSViewController { if !downloadsButton.isHidden { setDownloadButtonHidingTimer() } downloadsButton.isMouseDown = popovers.isDownloadsPopoverShown - + // If the user has selected Hide Downloads from the navigation bar context menu, and no downloads are active, then force it to be hidden // even if the timer is active. if updatingFromPinnedViewsNotification { @@ -512,7 +512,7 @@ final class NavigationBarViewController: NSViewController { if LocalPinningManager.shared.isPinned(.downloads) || DownloadListCoordinator.shared.hasActiveDownloads || popovers.isDownloadsPopoverShown { return } - + downloadsButton.isHidden = true } @@ -520,10 +520,10 @@ final class NavigationBarViewController: NSViewController { let menu = NSMenu() let title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .bookmarks) menu.addItem(withTitle: title, action: #selector(toggleBookmarksPanelPinning(_:)), keyEquivalent: "") - + bookmarkListButton.menu = menu bookmarkListButton.toolTip = UserText.bookmarksShortcutTooltip - + if LocalPinningManager.shared.isPinned(.bookmarks) { bookmarkListButton.isHidden = false } else { @@ -609,15 +609,15 @@ extension NavigationBarViewController: MouseOverViewDelegate { } extension NavigationBarViewController: NSMenuDelegate { - + public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() - + let bookmarksBarTitle = PersistentAppInterfaceSettings.shared.showBookmarksBar ? UserText.hideBookmarksBar : UserText.showBookmarksBar menu.addItem(withTitle: bookmarksBarTitle, action: #selector(toggleBookmarksBar), keyEquivalent: "B") - + menu.addItem(NSMenuItem.separator()) - + let autofillTitle = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .autofill) menu.addItem(withTitle: autofillTitle, action: #selector(toggleAutofillPanelPinning), keyEquivalent: "A") @@ -627,22 +627,22 @@ extension NavigationBarViewController: NSMenuDelegate { let downloadsTitle = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .downloads) menu.addItem(withTitle: downloadsTitle, action: #selector(toggleDownloadsPanelPinning), keyEquivalent: "J") } - + @objc private func toggleBookmarksBar(_ sender: NSMenuItem) { PersistentAppInterfaceSettings.shared.showBookmarksBar.toggle() } - + @objc private func toggleAutofillPanelPinning(_ sender: NSMenuItem) { LocalPinningManager.shared.togglePinning(for: .autofill) } - + @objc private func toggleBookmarksPanelPinning(_ sender: NSMenuItem) { LocalPinningManager.shared.togglePinning(for: .bookmarks) } - + @objc private func toggleDownloadsPanelPinning(_ sender: NSMenuItem) { LocalPinningManager.shared.togglePinning(for: .downloads) @@ -670,11 +670,11 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { func optionsButtonMenuRequestedToggleBookmarksBar(_ menu: NSMenu) { PersistentAppInterfaceSettings.shared.showBookmarksBar.toggle() } - + func optionsButtonMenuRequestedBookmarkManagementInterface(_ menu: NSMenu) { WindowControllersManager.shared.showBookmarksTab() } - + func optionsButtonMenuRequestedBookmarkImportInterface(_ menu: NSMenu) { DataImportViewController.show() } @@ -732,12 +732,12 @@ extension NavigationBarViewController: DownloadsViewControllerDelegate { #if DEBUG || REVIEW extension NavigationBarViewController { - + fileprivate func addDebugNotificationListeners() { NotificationCenter.default.addObserver(forName: .ShowSaveCredentialsPopover, object: nil, queue: .main) { [weak self] _ in self?.showMockSaveCredentialsPopover() } - + NotificationCenter.default.addObserver(forName: .ShowCredentialsSavedPopover, object: nil, queue: .main) { [weak self] _ in self?.showMockCredentialsSavedPopover() } @@ -751,7 +751,7 @@ extension NavigationBarViewController { usingView: passwordManagementButton, withDelegate: self) } - + fileprivate func showMockCredentialsSavedPopover() { let account = SecureVaultModels.WebsiteAccount(title: nil, username: "example-username", domain: "example.com") let mockCredentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)!) @@ -761,6 +761,6 @@ extension NavigationBarViewController { usingView: passwordManagementButton, withDelegate: self) } - + } #endif diff --git a/DuckDuckGo/Onboarding/View/ActionSpeech.swift b/DuckDuckGo/Onboarding/View/ActionSpeech.swift index 228f4eb371..2380af1de9 100644 --- a/DuckDuckGo/Onboarding/View/ActionSpeech.swift +++ b/DuckDuckGo/Onboarding/View/ActionSpeech.swift @@ -36,7 +36,7 @@ struct ActionSpeech: View { typingFinished = true } } - + HStack(spacing: 12) { Button(UserText.onboardingNotNowButton) { diff --git a/DuckDuckGo/Onboarding/View/OnboardingViewController.swift b/DuckDuckGo/Onboarding/View/OnboardingViewController.swift index 541635e909..0b294acedf 100644 --- a/DuckDuckGo/Onboarding/View/OnboardingViewController.swift +++ b/DuckDuckGo/Onboarding/View/OnboardingViewController.swift @@ -30,7 +30,7 @@ final class OnboardingViewController: NSViewController { // swiftlint:enable force_cast return controller } - + @IBOutlet var backgroundImageView: NSImageView! { didSet { // NSImageView magic to improve image resizing performance. diff --git a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift index fad86793d2..e8f53a5895 100644 --- a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift +++ b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift @@ -97,7 +97,7 @@ final class OnboardingViewModel: ObservableObject { func skipTyping() { skipTypingRequested = true } - + func onboardingReshown() { if onboardingFinished { typingDisabled = true diff --git a/DuckDuckGo/Password Managers/Bitwarden/Model/BWManager.swift b/DuckDuckGo/Password Managers/Bitwarden/Model/BWManager.swift index 68730e2e83..27a2bf3ed9 100644 --- a/DuckDuckGo/Password Managers/Bitwarden/Model/BWManager.swift +++ b/DuckDuckGo/Password Managers/Bitwarden/Model/BWManager.swift @@ -517,7 +517,7 @@ final class BWManager: BWManagement, ObservableObject { return "2.\(encryptedData.iv.base64EncodedString())|\(encryptedData.data.base64EncodedString())|\(encryptedData.hmac.base64EncodedString())" } - + // MARK: - Encryption lazy var encryption = BWEncryption() diff --git a/DuckDuckGo/Password Managers/Bitwarden/Model/BWStatus.swift b/DuckDuckGo/Password Managers/Bitwarden/Model/BWStatus.swift index 887d468665..79dfeba7b8 100644 --- a/DuckDuckGo/Password Managers/Bitwarden/Model/BWStatus.swift +++ b/DuckDuckGo/Password Managers/Bitwarden/Model/BWStatus.swift @@ -37,7 +37,7 @@ enum BWStatus: Equatable { // There is handshake necessary in order to receive the shared key case missingHandshake - + // Waiting for the handshake approval in Bitwarden case waitingForHandshakeApproval @@ -52,7 +52,7 @@ enum BWStatus: Equatable { case connected(vault: BWVault) case error(error: BWError) - + var isConnected: Bool { switch self { case .connected: return true diff --git a/DuckDuckGo/Password Managers/Bitwarden/Services/BWInstallationService.swift b/DuckDuckGo/Password Managers/Bitwarden/Services/BWInstallationService.swift index 2d38264b68..38f397cf54 100644 --- a/DuckDuckGo/Password Managers/Bitwarden/Services/BWInstallationService.swift +++ b/DuckDuckGo/Password Managers/Bitwarden/Services/BWInstallationService.swift @@ -125,9 +125,9 @@ final class LocalBitwardenInstallationService: BWInstallationService { private func getModificationDate(for dataFileURL: URL) -> Date? { return try? FileManager.default.attributesOfItem(atPath: dataFileURL.path)[FileAttributeKey.modificationDate] as? Date } - + func openBitwarden() { NSWorkspace.shared.open(bundleUrl) } - + } diff --git a/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenView.swift b/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenView.swift index 39e00070e8..6343fd3288 100644 --- a/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenView.swift +++ b/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenView.swift @@ -20,7 +20,7 @@ import Foundation import SwiftUI struct ConnectBitwardenView: View { - + private enum Constants { static let headerPadding = 20.0 static let bodyPadding = 20.0 @@ -36,17 +36,17 @@ struct ConnectBitwardenView: View { headerHeight + 2 * Constants.headerPadding + viewHeight + 4 * Constants.bodyPadding + spacerHeight + buttonsHeight } } - + @EnvironmentObject var viewModel: ConnectBitwardenViewModel - + let sizeChanged: (CGFloat) -> Void - + @State var viewSize: ViewSize = .init() { didSet { sizeChanged(viewSize.totalHeight) } } - + var body: some View { VStack { VStack(spacing: Constants.bodyPadding) { @@ -58,7 +58,7 @@ struct ConnectBitwardenView: View { } } ) - + bodyView(for: viewModel.viewState) .frame(maxWidth: .infinity) .background( @@ -70,7 +70,7 @@ struct ConnectBitwardenView: View { ) } .padding(Constants.headerPadding) - + Spacer() .background( GeometryReader { proxy in @@ -79,7 +79,7 @@ struct ConnectBitwardenView: View { } } ) - + ButtonsView() .background( GeometryReader { proxy in @@ -90,7 +90,7 @@ struct ConnectBitwardenView: View { ) } } - + @ViewBuilder private func bodyView(for state: ConnectBitwardenViewModel.ViewState) -> some View { switch viewModel.viewState { case .disclaimer: ConnectToBitwardenDisclaimerView() @@ -102,81 +102,81 @@ struct ConnectBitwardenView: View { case .connectedToBitwarden: ConnectedToBitwardenView() } } - + } struct BitwardenTitleView: View { - + var body: some View { - + HStack(spacing: 10) { Image("BitwardenLogo") .resizable() .frame(width: 32, height: 32) - + Text(UserText.connectToBitwarden) .font(.system(size: 18, weight: .semibold)) - + Spacer() } } - + } private struct ConnectToBitwardenDisclaimerView: View { - + var body: some View { VStack(alignment: .leading, spacing: 10) { Text(UserText.connectToBitwardenDescription) - + Text(UserText.connectToBitwardenPrivacy) .font(.system(size: 13, weight: .bold)) .padding(.top, 10) - + HStack { Image("BitwardenLock") Text(UserText.bitwardenCommunicationInfo) } - + HStack { Image("BitwardenClock") Text(UserText.bitwardenHistoryInfo) } } } - + } private struct BitwardenInstallationDetectionView: View { - + @EnvironmentObject var viewModel: ConnectBitwardenViewModel - + let bitwardenDetected: Bool let bitwardenNeedsUpdate: Bool - + var body: some View { VStack(alignment: .leading, spacing: 10) { Text(UserText.installBitwarden) .font(.system(size: 13, weight: .bold)) - + HStack { NumberedBadge(value: 1) Text(UserText.installBitwardenInfo) - + Spacer() } - + HStack { NumberedBadge(value: 2) - + Text(UserText.afterBitwardenInstallationInfo) - + Spacer() } - + Button(action: { viewModel.process(action: .openBitwardenProductPage) }, label: { @@ -184,7 +184,7 @@ private struct BitwardenInstallationDetectionView: View { }) .buttonStyle(PlainButtonStyle()) .frame(width: 156, height: 40) - + if bitwardenDetected { if bitwardenNeedsUpdate { HStack { @@ -201,7 +201,7 @@ private struct BitwardenInstallationDetectionView: View { } else { HStack { ActivityIndicator(isAnimating: .constant(true), style: .spinning) - + Text(UserText.lookingForBitwarden) } } @@ -209,20 +209,20 @@ private struct BitwardenInstallationDetectionView: View { .frame(maxWidth: .infinity) } - + } private struct ConnectToBitwardenView: View { - + @EnvironmentObject var viewModel: ConnectBitwardenViewModel - + let canConnect: Bool var body: some View { VStack(alignment: .leading, spacing: 15) { Text(UserText.allowIntegration) .font(.system(size: 13, weight: .bold)) - + HStack { NumberedBadge(value: 1) Text(UserText.openBitwardenAndLogInOrUnlock) @@ -237,37 +237,37 @@ private struct ConnectToBitwardenView: View { } Spacer().frame(height: 2) - + HStack { NumberedBadge(value: 2) Text(UserText.selectBitwardenPreferences) Spacer() } - + HStack { NumberedBadge(value: 3) Text(UserText.scrollToFindAppSettings) Spacer() } - + HStack { NumberedBadge(value: 4) Text(UserText.checkAllowIntegration) Spacer() } - + Image("BitwardenSettingsIllustration") - + if canConnect { HStack { Image("SuccessCheckmark") - + Text(UserText.bitwardenIsReadyToConnect) - + Spacer() } } else { - + HStack { ActivityIndicator(isAnimating: .constant(true), style: .spinning) .frame(maxWidth: 8, maxHeight: 8) @@ -277,35 +277,35 @@ private struct ConnectToBitwardenView: View { } } } - + } private struct ConnectedToBitwardenView: View { var body: some View { VStack(alignment: .leading) { - + Text(UserText.bitwardenIntegrationComplete) .font(.system(size: 13, weight: .bold)) - + HStack { Image("SuccessCheckmark") Text(UserText.bitwardenIntegrationCompleteInfo) - + Spacer() } } .frame(maxWidth: .infinity) } - + } // MARK: - Reusable Views private struct NumberedBadge: View { - + let value: Int var body: some View { @@ -317,26 +317,26 @@ private struct NumberedBadge: View { } .frame(width: 20, height: 20) } - + } private struct ButtonsView: View { - + @EnvironmentObject var viewModel: ConnectBitwardenViewModel - + var body: some View { - + Divider() - + HStack { Spacer() - + if viewModel.viewState.cancelButtonVisible { Button(UserText.cancel) { viewModel.process(action: .cancel) } } - + if #available(macOS 11.0, *) { Button(viewModel.viewState.confirmButtonTitle) { viewModel.process(action: .confirm) @@ -352,13 +352,13 @@ private struct ButtonsView: View { } .padding([.trailing, .bottom], 16) .padding(.top, 10) - + } - + } struct ActivityIndicator: NSViewRepresentable { - + @Binding var isAnimating: Bool let style: NSProgressIndicator.Style @@ -377,5 +377,5 @@ struct ActivityIndicator: NSViewRepresentable { nsView.stopAnimation(nil) } } - + } diff --git a/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewController.swift b/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewController.swift index 826d8b5b49..d7bc1366cc 100644 --- a/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewController.swift +++ b/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewController.swift @@ -21,22 +21,22 @@ import Combine import SwiftUI final class ConnectBitwardenViewController: NSViewController { - + private let defaultSize = CGSize(width: 550, height: 280) private let viewModel = ConnectBitwardenViewModel(bitwardenManager: BWManager.shared) - + var setupFlowCancellationHandler: (() -> Void)? - + private var heightConstraint: NSLayoutConstraint? - + public override func loadView() { view = NSView(frame: NSRect(origin: CGPoint.zero, size: defaultSize)) } - + public override func viewDidLoad() { super.viewDidLoad() viewModel.delegate = self - + let connectBitwardenView = ConnectBitwardenView { newHeight in self.updateViewHeight(height: newHeight) } @@ -44,10 +44,10 @@ final class ConnectBitwardenViewController: NSViewController { let hostingView = NSHostingView(rootView: connectBitwardenView.environmentObject(self.viewModel)) hostingView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hostingView) - + let heightConstraint = hostingView.heightAnchor.constraint(equalToConstant: defaultSize.height) self.heightConstraint = heightConstraint - + NSLayoutConstraint.activate([ heightConstraint, hostingView.widthAnchor.constraint(equalToConstant: defaultSize.width), @@ -57,15 +57,15 @@ final class ConnectBitwardenViewController: NSViewController { hostingView.rightAnchor.constraint(equalTo: view.rightAnchor) ]) } - + private func updateViewHeight(height: CGFloat) { heightConstraint?.constant = height } - + } extension ConnectBitwardenViewController: ConnectBitwardenViewModelDelegate { - + func connectBitwardenViewModelDismissedView(_ viewModel: ConnectBitwardenViewModel, canceled: Bool) { if canceled { setupFlowCancellationHandler?() @@ -73,5 +73,5 @@ extension ConnectBitwardenViewController: ConnectBitwardenViewModelDelegate { dismiss() } - + } diff --git a/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewModel.swift b/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewModel.swift index 736c28c5db..74a51b3954 100644 --- a/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewModel.swift +++ b/DuckDuckGo/Password Managers/Bitwarden/View/ConnectBitwardenViewModel.swift @@ -22,37 +22,37 @@ import os.log import AppKit protocol ConnectBitwardenViewModelDelegate: AnyObject { - + func connectBitwardenViewModelDismissedView(_ viewModel: ConnectBitwardenViewModel, canceled: Bool) - + } final class ConnectBitwardenViewModel: ObservableObject { - + enum ViewState { - + // Initial state: case disclaimer - + // Bitwarden installation: case lookingForBitwarden case oldVersion case bitwardenFound - + // Bitwarden connection: case waitingForConnectionPermission case connectToBitwarden - + // Final state: case connectedToBitwarden - + var canContinue: Bool { switch self { case .lookingForBitwarden, .oldVersion, .waitingForConnectionPermission: return false default: return true } } - + var confirmButtonTitle: String { switch self { case .disclaimer, .lookingForBitwarden, .oldVersion, .bitwardenFound: return "Next" @@ -60,31 +60,31 @@ final class ConnectBitwardenViewModel: ObservableObject { case .connectedToBitwarden: return "OK" } } - + var cancelButtonVisible: Bool { return self != .connectedToBitwarden } - + } - + enum ViewAction { case cancel case confirm case openBitwarden case openBitwardenProductPage } - + private enum Constants { static let bitwardenAppStoreURL = URL(string: "macappstores://apps.apple.com/app/bitwarden/id1352778147")! } - + weak var delegate: ConnectBitwardenViewModelDelegate? - + @Published private(set) var viewState: ViewState = .disclaimer @Published private(set) var error: Error? private let bitwardenManager: BWManagement - + private var bitwardenManagerStatusCancellable: AnyCancellable? init(bitwardenManager: BWManagement) { @@ -138,13 +138,13 @@ final class ConnectBitwardenViewModel: ObservableObject { } else if viewState == .disclaimer { viewState = .lookingForBitwarden } - + case .cancel: delegate?.connectBitwardenViewModelDismissedView(self, canceled: true) - + case .openBitwarden: bitwardenManager.openBitwarden() - + case .openBitwardenProductPage: NSWorkspace.shared.open(Constants.bitwardenAppStoreURL) } diff --git a/DuckDuckGo/Password Managers/PasswordManagerCoordinator.swift b/DuckDuckGo/Password Managers/PasswordManagerCoordinator.swift index f5569f1807..2c4d6cec56 100644 --- a/DuckDuckGo/Password Managers/PasswordManagerCoordinator.swift +++ b/DuckDuckGo/Password Managers/PasswordManagerCoordinator.swift @@ -45,18 +45,18 @@ final class PasswordManagerCoordinator: PasswordManagerCoordinating { var name: String { return "bitwarden" } - + var displayName: String { return "Bitwarden" } - + var username: String? { if case let .connected(vault: vault) = bitwardenManagement.status { return vault.email } return nil } - + var isLocked: Bool { switch bitwardenManagement.status { case .connected(vault: let vault): return vault.status == .locked diff --git a/DuckDuckGo/Permissions/Model/PermissionManager.swift b/DuckDuckGo/Permissions/Model/PermissionManager.swift index f77291cbce..da03f3421d 100644 --- a/DuckDuckGo/Permissions/Model/PermissionManager.swift +++ b/DuckDuckGo/Permissions/Model/PermissionManager.swift @@ -22,7 +22,6 @@ import os.log protocol PermissionManagerProtocol: AnyObject { - // swiftlint:disable:next large_tuple typealias PublishedPermission = (domain: String, permissionType: PermissionType, decision: PersistedPermissionDecision) var permissionPublisher: AnyPublisher { get } @@ -109,7 +108,7 @@ final class PermissionManager: PermissionManagerProtocol { } store.clear(except: permissions.values.reduce(into: [StoredPermission](), { $0.append(contentsOf: $1.values) - }), completionHandler: { _ in + }), completionHandler: { _ in completion() }) } diff --git a/DuckDuckGo/Permissions/Model/PermissionState.swift b/DuckDuckGo/Permissions/Model/PermissionState.swift index 1a8a89828c..d57c39cea5 100644 --- a/DuckDuckGo/Permissions/Model/PermissionState.swift +++ b/DuckDuckGo/Permissions/Model/PermissionState.swift @@ -54,7 +54,7 @@ enum PermissionState: Equatable { if case .denied = self { return true } return false } - + } extension Optional where Wrapped == PermissionState { diff --git a/DuckDuckGo/Permissions/Model/Permissions.swift b/DuckDuckGo/Permissions/Model/Permissions.swift index 43170c5377..540d5a6275 100644 --- a/DuckDuckGo/Permissions/Model/Permissions.swift +++ b/DuckDuckGo/Permissions/Model/Permissions.swift @@ -30,7 +30,7 @@ extension Dictionary where Key == PermissionType, Value == PermissionState { self[.microphone] = newValue } } - + var camera: PermissionState? { get { self[.camera] diff --git a/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift b/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift index 8e11126fad..44c5ee1635 100644 --- a/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift +++ b/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift @@ -122,34 +122,34 @@ private struct BorderView: View { let isSelected: Bool let cornerRadius: CGFloat let size: CGFloat - + private var borderColor: Color { isSelected ? Color(TabShadowConfig.colorName) : .clear } - + private var bottomLineColor: Color { isSelected ? Color("InterfaceBackgroundColor") : Color(TabShadowConfig.colorName) } - + private var cornerPixelsColor: Color { isSelected ? .clear : bottomLineColor } - + var body: some View { ZStack { CustomRoundedCornersShape(inset: 0, tl: cornerRadius, tr: cornerRadius, bl: 0, br: 0) .strokeBorder(borderColor, lineWidth: size) - + VStack { Spacer() HStack { Spacer().frame(width: 1, height: size, alignment: .leading) .background(cornerPixelsColor) - + Rectangle() .fill(bottomLineColor) .frame(height: size, alignment: .leading) - + Spacer().frame(width: 1, height: size, alignment: .trailing) .background(cornerPixelsColor) } diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift index 5f82f6cdc4..e81b82b5fb 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferences.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferences.swift @@ -103,7 +103,7 @@ final class AutofillPreferences: AutofillPreferencesPersistor { @UserDefaultsWrapper(key: .askToSavePaymentMethods, defaultValue: true) var askToSavePaymentMethods: Bool - + var passwordManager: PasswordManager { get { return PasswordManager(rawValue: selectedPasswordManager) ?? .duckduckgo diff --git a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift index 20e6f85552..b8a47e8b01 100644 --- a/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/AutofillPreferencesModel.swift @@ -49,7 +49,7 @@ final class AutofillPreferencesModel: ObservableObject { persistor.autoLockThreshold = autoLockThreshold } } - + @Published private(set) var passwordManager: PasswordManager { didSet { persistor.passwordManager = passwordManager @@ -91,7 +91,7 @@ final class AutofillPreferencesModel: ObservableObject { } } } - + func passwordManagerSettingsChange(passwordManager: PasswordManager) { self.passwordManager = passwordManager } @@ -120,13 +120,13 @@ final class AutofillPreferencesModel: ObservableObject { private var persistor: AutofillPreferencesPersistor private var userAuthenticator: UserAuthenticating private let bitwardenInstallationService: BWInstallationService - + // MARK: - Password Manager - + func presentBitwardenSetupFlow() { let connectBitwardenViewController = ConnectBitwardenViewController(nibName: nil, bundle: nil) let connectBitwardenWindowController = connectBitwardenViewController.wrappedInWindowController() - + connectBitwardenViewController.setupFlowCancellationHandler = { [weak self] in self?.passwordManager = .duckduckgo } @@ -143,7 +143,7 @@ final class AutofillPreferencesModel: ObservableObject { self?.isBitwardenSetupFlowPresented = false } } - + func openBitwarden() { PasswordManagerCoordinator.shared.openPasswordManager() } diff --git a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift index 85a5b5aa82..f3c812e4fc 100644 --- a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift @@ -31,7 +31,7 @@ protocol DownloadsPreferencesPersistor { struct DownloadsPreferencesUserDefaultsPersistor: DownloadsPreferencesPersistor { @UserDefaultsWrapper(key: .selectedDownloadLocationKey, defaultValue: nil) var selectedDownloadLocation: String? - + @UserDefaultsWrapper(key: .lastUsedCustomDownloadLocation, defaultValue: nil) var lastUsedCustomDownloadLocation: String? @@ -67,19 +67,19 @@ final class DownloadsPreferences: ObservableObject { } return nil } - + var effectiveDownloadLocation: URL? { if let selectedLocationURL = alwaysRequestDownloadLocation ? validatedDownloadLocation(persistor.lastUsedCustomDownloadLocation) : validatedDownloadLocation(persistor.selectedDownloadLocation) { return selectedLocationURL } return Self.defaultDownloadLocation() } - + var lastUsedCustomDownloadLocation: URL? { get { persistor.lastUsedCustomDownloadLocation?.url } - + set { defer { objectWillChange.send() @@ -99,7 +99,7 @@ final class DownloadsPreferences: ObservableObject { get { persistor.selectedDownloadLocation?.url } - + set { defer { objectWillChange.send() @@ -119,7 +119,7 @@ final class DownloadsPreferences: ObservableObject { get { persistor.alwaysRequestDownloadLocation } - + set { persistor.alwaysRequestDownloadLocation = newValue objectWillChange.send() @@ -137,7 +137,7 @@ final class DownloadsPreferences: ObservableObject { init(persistor: DownloadsPreferencesPersistor = DownloadsPreferencesUserDefaultsPersistor()) { self.persistor = persistor - + // Fix the selected download location if it needs it if selectedDownloadLocation == nil || !Self.isDownloadLocationValid(selectedDownloadLocation!) { selectedDownloadLocation = Self.defaultDownloadLocation() diff --git a/DuckDuckGo/Preferences/View/Preferences.swift b/DuckDuckGo/Preferences/View/Preferences.swift index 9076095ea9..fa5a7207e2 100644 --- a/DuckDuckGo/Preferences/View/Preferences.swift +++ b/DuckDuckGo/Preferences/View/Preferences.swift @@ -21,10 +21,10 @@ import SwiftUI enum Preferences { struct Section: View where Content: View { - + let spacing: CGFloat @ViewBuilder let content: () -> Content - + init(spacing: CGFloat = 12, @ViewBuilder content: @escaping () -> Content) { self.spacing = spacing self.content = content diff --git a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift index 2c1d4f92e0..7d61cad517 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAutofillView.swift @@ -41,7 +41,7 @@ extension Preferences { model.passwordManagerSettingsChange(passwordManager: newValue) } } - + var isAutoLockEnabledBinding: Binding { .init { model.isAutoLockEnabled @@ -64,7 +64,7 @@ extension Preferences { .font(Const.Fonts.preferencePaneTitle) // Password Manager: - + Section(spacing: 0) { Text(UserText.autofillPasswordManager) .font(Const.Fonts.preferencePaneSectionHeader) @@ -90,9 +90,9 @@ extension Preferences { .offset(x: Const.autoLockWarningOffset) } } - + // Ask to Save: - + Section(spacing: 0) { Text(UserText.autofillAskToSave) .font(Const.Fonts.preferencePaneSectionHeader) @@ -115,7 +115,7 @@ extension Preferences { } // Auto-Lock: - + Section(spacing: 0) { Text(UserText.autofillAutoLock) .font(Const.Fonts.preferencePaneSectionHeader) @@ -238,17 +238,17 @@ extension Preferences { } private struct BitwardenStatusView: View { - + struct ButtonValue { let title: String let action: () -> Void } - + enum IconType { case success case warning case error - + fileprivate var imageName: String { switch self { case .success: return "SuccessCheckmark" @@ -257,13 +257,13 @@ private struct BitwardenStatusView: View { } } } - + let iconType: IconType let title: String let buttonValue: ButtonValue? - + var body: some View { - + HStack { HStack { Image(iconType.imageName) @@ -277,12 +277,12 @@ private struct BitwardenStatusView: View { RoundedRectangle(cornerRadius: 5) .stroke(Color.black.opacity(0.08), lineWidth: 1) ) - + if let buttonValue = buttonValue { Button(buttonValue.title, action: buttonValue.action) } } - + } - + } diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 8377d66a15..5db8b092cf 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -23,7 +23,7 @@ import Combine final class PreferencesViewController: NSViewController { weak var delegate: BrowserTabSelectionDelegate? - + let model = PreferencesSidebarModel(includePrivatePlayer: PrivatePlayer.shared.isAvailable) private var selectedTabIndexCancellable: AnyCancellable? private var selectedPreferencePaneCancellable: AnyCancellable? diff --git a/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift b/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift index ed48448a78..2f6905409f 100644 --- a/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift +++ b/DuckDuckGo/Privacy Dashboard/ContentBlockingRulesUpdateObserver.swift @@ -21,39 +21,39 @@ import Combine import BrowserServicesKit final class ContentBlockingRulesUpdateObserver { - + @Published public private(set) var pendingUpdates = [String: String]() - + public private(set) weak var tabViewModel: TabViewModel? private var onPendingUpdates: (() -> Void)? private var rulesRecompilationCancellable: AnyCancellable? - + public func updateTabViewModel(_ tabViewModel: TabViewModel, onPendingUpdates: @escaping () -> Void) { rulesRecompilationCancellable?.cancel() rulesRecompilationCancellable = nil - + self.tabViewModel = tabViewModel self.onPendingUpdates = onPendingUpdates - + bindContentBlockingRulesRecompilation(publisher: (ContentBlocking.shared as? AppContentBlocking)!.userContentUpdating.userContentBlockingAssets) } - + public func didStartCompilation(for domain: String, token: ContentBlockerRulesManager.CompletionToken ) { pendingUpdates[token] = domain onPendingUpdates?() } - + private func bindContentBlockingRulesRecompilation(publisher: Pub) where Pub.Output == UserContentUpdating.NewContent, Pub.Failure == Never { - + rulesRecompilationCancellable = publisher .compactMap(\.nonEmptyCompletionTokens) .receive(on: RunLoop.main) .sink { [weak self] completionTokens in dispatchPrecondition(condition: .onQueue(.main)) - + guard let self = self, !self.pendingUpdates.isEmpty else { return } - + var didUpdate = false for token in completionTokens { // swiftlint:disable:next for_where @@ -61,7 +61,7 @@ final class ContentBlockingRulesUpdateObserver { didUpdate = true } } - + if didUpdate { self.tabViewModel?.reload() self.onPendingUpdates?() diff --git a/DuckDuckGo/Privacy Dashboard/PrivacyDashboardPermissionHandler.swift b/DuckDuckGo/Privacy Dashboard/PrivacyDashboardPermissionHandler.swift index 61b146cf98..f1734c4a6f 100644 --- a/DuckDuckGo/Privacy Dashboard/PrivacyDashboardPermissionHandler.swift +++ b/DuckDuckGo/Privacy Dashboard/PrivacyDashboardPermissionHandler.swift @@ -23,38 +23,38 @@ import PrivacyDashboard typealias PrivacyDashboardPermissionAuthorizationState = [(permission: PermissionType, state: PermissionAuthorizationState)] final class PrivacyDashboardPermissionHandler { - + private weak var tabViewModel: TabViewModel? private var onPermissionChange: (([AllowedPermission]) -> Void)? private var cancellables = Set() - + public func updateTabViewModel(_ tabViewModel: TabViewModel, onPermissionChange: @escaping ([AllowedPermission]) -> Void) { cancellables.removeAll() - + self.tabViewModel = tabViewModel self.onPermissionChange = onPermissionChange - + subscribeToPermissions() } - + public func setPermission(with permissionName: String, paused: Bool) { guard let permission = PermissionType(rawValue: permissionName) else { return } - + tabViewModel?.tab.permissions.set([permission], muted: paused) } - + public func setPermissionAuthorization(authorizationState: PermissionAuthorizationState, domain: String, permissionName: String) { guard let permission = PermissionType(rawValue: permissionName) else { return } - + PermissionManager.shared.setPermission(authorizationState.persistedPermissionDecision, forDomain: domain, permissionType: permission) } - + private func subscribeToPermissions() { tabViewModel?.$usedPermissions.receive(on: DispatchQueue.main).sink { [weak self] _ in self?.updatePermissions() }.store(in: &cancellables) } - + private func updatePermissions() { guard let usedPermissions = tabViewModel?.usedPermissions else { assertionFailure("PrivacyDashboardViewController: tabViewModel not set") @@ -64,7 +64,7 @@ final class PrivacyDashboardPermissionHandler { onPermissionChange?([]) return } - + let authorizationState: PrivacyDashboardPermissionAuthorizationState authorizationState = PermissionManager.shared.persistedPermissionTypes.union(usedPermissions.keys).compactMap { permissionType in guard PermissionManager.shared.hasPermissionPersisted(forDomain: domain, permissionType: permissionType) @@ -75,9 +75,9 @@ final class PrivacyDashboardPermissionHandler { let decision = PermissionManager.shared.permission(forDomain: domain, permissionType: permissionType) return (permissionType, PermissionAuthorizationState(decision: decision)) } - + var allowedPermissions: [AllowedPermission] = [] - + allowedPermissions = authorizationState.map { item in AllowedPermission(key: item.permission.rawValue, icon: item.permission.jsStyle, @@ -88,10 +88,10 @@ final class PrivacyDashboardPermissionHandler { options: makeOptions(for: item, domain: domain) ) } - + onPermissionChange?(allowedPermissions) } - + private func makeOptions(for item: (permission: PermissionType, state: PermissionAuthorizationState), domain: String) -> [[String: String]] { return PermissionAuthorizationState.allCases.compactMap { decision -> [String: String]? in // don't show Permanently Allow if can't persist Granted Decision @@ -119,7 +119,7 @@ extension PermissionType { return "externalScheme" } } - + var jsTitle: String { switch self { case .camera, .microphone, .geolocation, .popups: @@ -131,7 +131,7 @@ extension PermissionType { } extension PermissionAuthorizationState { - + init(decision: PersistedPermissionDecision) { switch decision { case .ask: @@ -142,7 +142,7 @@ extension PermissionAuthorizationState { self = .deny } } - + var persistedPermissionDecision: PersistedPermissionDecision { switch self { case .ask: return .ask @@ -150,7 +150,7 @@ extension PermissionAuthorizationState { case .deny: return .deny } } - + func localizedFormat(for permission: PermissionType) -> String { switch (permission, self) { case (.popups, .ask): diff --git a/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardPopover.swift b/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardPopover.swift index 646f2f815d..eba048f8c8 100644 --- a/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardPopover.swift +++ b/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardPopover.swift @@ -48,7 +48,7 @@ final class PrivacyDashboardPopover: NSPopover { contentViewController = controller } // swiftlint:enable force_cast - + func setPreferredMaxHeight(_ height: CGFloat) { viewController.setPreferredMaxHeight(height - 40) // Account for popover arrow height } diff --git a/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift index 28606fc374..cc88f0cb57 100644 --- a/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/Privacy Dashboard/View/PrivacyDashboardViewController.swift @@ -40,53 +40,53 @@ final class PrivacyDashboardViewController: NSViewController { /// The animation only needs to run when transitioning between views in the popover, so this is used to track when to run the animation. /// This should be set to true any time the popover is displayed (i.e., reset to true when dismissing the popover), and false after the initial resize pass is complete. @Published private var shouldAnimateHeightChange: Bool = false - + @Published private var currentContentHeight: Int = Int(Constants.initialContentHeight) private var currentContentHeightCancellable: AnyCancellable? - + private var preferredMaxHeight: CGFloat = Constants.initialContentHeight func setPreferredMaxHeight(_ height: CGFloat) { guard height > Constants.initialContentHeight else { return } - + preferredMaxHeight = height } - + public func updateTabViewModel(_ tabViewModel: TabViewModel) { privacyDashboardController.updatePrivacyInfo(tabViewModel.tab.privacyInfo) - + rulesUpdateObserver.updateTabViewModel(tabViewModel, onPendingUpdates: { [weak self] in self?.sendPendingUpdates() }) - + websiteBreakageReporter.updateTabViewModel(tabViewModel) - + permissionHandler.updateTabViewModel(tabViewModel) { [weak self] allowedPermissions in self?.privacyDashboardController.allowedPermissions = allowedPermissions } } - + public override func viewDidLoad() { super.viewDidLoad() - + initWebView() privacyDashboardController.setup(for: webView) setupHeightChangeHandler() } - + override func viewWillAppear() { super.viewWillAppear() privacyDashboardController.delegate = self privacyDashboardController.preferredLocale = "en" // fixed until app is localised - + webView.reload() } - + override func viewDidAppear() { super.viewDidAppear() - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { self.shouldAnimateHeightChange = true } @@ -94,17 +94,17 @@ final class PrivacyDashboardViewController: NSViewController { override func viewWillDisappear() { super.viewWillDisappear() - + privacyDashboardController.delegate = nil shouldAnimateHeightChange = false } - + override func viewDidDisappear() { super.viewDidDisappear() - + currentContentHeight = Int(Constants.initialContentHeight) } - + private func initWebView() { let configuration = WKWebViewConfiguration() @@ -115,13 +115,13 @@ final class PrivacyDashboardViewController: NSViewController { let webView = PrivacyDashboardWebView(frame: .zero, configuration: configuration) self.webView = webView view.addAndLayout(webView) - + view.topAnchor.constraint(equalTo: webView.topAnchor).isActive = true - + contentHeightConstraint = view.heightAnchor.constraint(equalToConstant: Constants.initialContentHeight) contentHeightConstraint.isActive = true } - + private func setupHeightChangeHandler() { currentContentHeightCancellable = $currentContentHeight .combineLatest($shouldAnimateHeightChange) @@ -132,7 +132,7 @@ final class PrivacyDashboardViewController: NSViewController { self?.onHeightChange(height, shouldAnimate: shouldAnimate) }) } - + public func isPendingUpdates() -> Bool { return !rulesUpdateObserver.pendingUpdates.isEmpty } @@ -145,12 +145,12 @@ final class PrivacyDashboardViewController: NSViewController { privacyDashboardController.didFinishRulesCompilation() } } - + private func isPendingUpdatesForCurrentDomain() -> Bool { guard let domain = privacyDashboardController.privacyInfo?.url.host else { return false } return rulesUpdateObserver.pendingUpdates.values.contains(domain) } - + private func onHeightChange(_ height: Int, shouldAnimate: Bool) { var height = CGFloat(height) if height > self.preferredMaxHeight { @@ -170,7 +170,7 @@ final class PrivacyDashboardViewController: NSViewController { } extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didChangeProtectionSwitch isEnabled: Bool) { guard let domain = privacyDashboardController.privacyInfo?.url.host else { return @@ -186,7 +186,7 @@ extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { let completionToken = ContentBlocking.shared.contentBlockingManager.scheduleCompilation() rulesUpdateObserver.didStartCompilation(for: domain, token: completionToken) } - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestOpenUrlInNewTab url: URL) { guard let tabCollection = WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel else { @@ -195,21 +195,21 @@ extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { } tabCollection.appendNewTab(with: .url(url), selected: true) } - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didSetHeight height: Int) { currentContentHeight = height } - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestSubmitBrokenSiteReportWithCategory category: String, description: String) { websiteBreakageReporter.reportBreakage(category: category, description: description) } - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didSetPermission permissionName: String, to state: PermissionAuthorizationState) { guard let domain = self.privacyDashboardController.privacyInfo?.url.host else { return } permissionHandler.setPermissionAuthorization(authorizationState: state, domain: domain, permissionName: permissionName) } - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, setPermission permissionName: String, paused: Bool) { permissionHandler.setPermission(with: permissionName, paused: paused) } diff --git a/DuckDuckGo/Privacy Dashboard/WebsiteBreakageReporter.swift b/DuckDuckGo/Privacy Dashboard/WebsiteBreakageReporter.swift index 809e02d20d..b7f32d3ad2 100644 --- a/DuckDuckGo/Privacy Dashboard/WebsiteBreakageReporter.swift +++ b/DuckDuckGo/Privacy Dashboard/WebsiteBreakageReporter.swift @@ -19,28 +19,28 @@ import Foundation final class WebsiteBreakageReporter { - + private weak var tabViewModel: TabViewModel? - + public func updateTabViewModel(_ tabViewModel: TabViewModel) { self.tabViewModel = tabViewModel } - + public func reportBreakage(category: String, description: String) { let websiteBreakage = makeWebsiteBreakage(category: category, description: description, currentTab: tabViewModel?.tab) let websiteBreakageSender = WebsiteBreakageSender() websiteBreakageSender.sendWebsiteBreakage(websiteBreakage) } - + private func makeWebsiteBreakage(category: String, description: String, currentTab: Tab?) -> WebsiteBreakage { // ⚠️ To limit privacy risk, site URL is trimmed to not include query and fragment let currentURL = currentTab?.content.url?.trimmingQueryItemsAndFragment()?.absoluteString ?? "" - + let blockedTrackerDomains = currentTab?.privacyInfo?.trackerInfo.trackersBlocked.compactMap { $0.domain } ?? [] let installedSurrogates = currentTab?.privacyInfo?.trackerInfo.installedSurrogates.map {$0} ?? [] let ampURL = currentTab?.linkProtection.lastAMPURLString ?? "" let urlParametersRemoved = currentTab?.linkProtection.urlParametersRemoved ?? false - + let websiteBreakage = WebsiteBreakage(category: WebsiteBreakage.Category(rawValue: category.lowercased()), description: description, siteUrlString: currentURL, diff --git a/DuckDuckGo/Secure Vault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/Secure Vault/Extensions/UserText+PasswordManager.swift index 77a1bad948..f79e03e49d 100644 --- a/DuckDuckGo/Secure Vault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/Secure Vault/Extensions/UserText+PasswordManager.swift @@ -22,7 +22,7 @@ extension UserText { static let pmSaveCredentialsEditableTitle = NSLocalizedString("pm.save-credentials.editable.title", value: "Save Login?", comment: "Title for the editable Save Credentials popover") static let pmSaveCredentialsNonEditableTitle = NSLocalizedString("pm.save-credentials.non-editable.title", value: "New Login Saved", comment: "Title for the non-editable Save Credentials popover") - + static let pmEmptyStateDefaultTitle = NSLocalizedString("pm.empty.default.title", value: "No Logins or Payment Methods saved yet", comment: "Label for default empty state title") static let pmEmptyStateDefaultDescription = NSLocalizedString("pm.empty.default.description", value: "If your logins are saved in another browser, you can import them into DuckDuckGo.", diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementIdentityModel.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementIdentityModel.swift index 1b0aa9190e..3451bef4fe 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementIdentityModel.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementIdentityModel.swift @@ -106,7 +106,7 @@ final class PasswordManagementIdentityModel: ObservableObject, PasswordManagemen isDirty = true } } - + @Published var addressStreet2: String = "" { didSet { isDirty = true diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift index 73b474b931..ecf3c1d57e 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift @@ -68,7 +68,7 @@ enum SecureVaultItem: Equatable, Identifiable, Comparable { return note.title } } - + var created: Date { switch self { case .account(let account): @@ -144,21 +144,21 @@ enum SecureVaultItem: Equatable, Identifiable, Comparable { return subtitle } } - + var firstCharacter: String { let defaultFirstCharacter = "#" guard let character = self.displayTitle.first else { return defaultFirstCharacter } - + if character.isLetter { return character.uppercased() } else { return defaultFirstCharacter } } - + var category: SecureVaultSorting.Category { switch self { case .account: return .logins @@ -167,12 +167,12 @@ enum SecureVaultItem: Equatable, Identifiable, Comparable { case .note: return .allItems } } - + func matches(category: SecureVaultSorting.Category) -> Bool { if category == .allItems { return true } - + return self.category == category } @@ -206,11 +206,11 @@ enum SecureVaultItem: Equatable, Identifiable, Comparable { /// Could maybe even abstract a bunch of this code to be more generic re-usable styled list for use elsewhere. final class PasswordManagementItemListModel: ObservableObject { let passwordManagerCoordinator: PasswordManagerCoordinating - + enum EmptyState { /// Displays nothing for the empty state. Used when data is still loading, or when filtering the All Items list. case none - + /// Displays an empty state which prompts the user to import data. Used when the user has no items of any type. case noData case logins @@ -249,7 +249,7 @@ final class PasswordManagementItemListModel: ObservableObject { guard oldValue != sortDescriptor else { return } - + updateFilteredData() selectFirst() } @@ -289,14 +289,14 @@ final class PasswordManagementItemListModel: ObservableObject { if let item = item, sortDescriptor.category != .allItems, item.category != sortDescriptor.category { sortDescriptor.category = item.category } - + let previous = selected selected = item - + if selected != nil { externalPasswordManagerSelected = false } - + if notify { onItemSelected(previous, item) } @@ -309,7 +309,7 @@ final class PasswordManagementItemListModel: ObservableObject { } } } - + func selectLoginWithDomainOrFirst(domain: String, notify: Bool = true) { for section in displayedItems { if let account = section.items.first(where: { $0.websiteAccount?.domain.droppingWwwPrefix() == domain.droppingWwwPrefix() }) { @@ -317,7 +317,7 @@ final class PasswordManagementItemListModel: ObservableObject { return } } - + selectFirst() } @@ -378,12 +378,12 @@ final class PasswordManagementItemListModel: ObservableObject { selected(item: nil) } } - + func clear() { update(items: []) filter = "" clearSelection() - + // Setting items to an empty array will typically show the No Data empty state, but this call is used when // the popover is closed so instead there should be no empty state. emptyState = .none @@ -421,18 +421,18 @@ final class PasswordManagementItemListModel: ObservableObject { return sections } - + private func calculateEmptyState() { guard !items.isEmpty else { emptyState = .noData return } - + guard displayedItems.isEmpty else { emptyState = .none return } - + switch sortDescriptor.category { case .allItems: emptyState = .none case .cards: emptyState = .creditCards diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementListSection.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementListSection.swift index 0b345a7f69..3ef13dc907 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementListSection.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementListSection.swift @@ -19,7 +19,7 @@ import Foundation struct PasswordManagementListSection { - + static var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMM yyyy" @@ -30,25 +30,25 @@ struct PasswordManagementListSection { static func < (lhs: PasswordManagementListSection.DateMetadata, rhs: PasswordManagementListSection.DateMetadata) -> Bool { return (lhs.month, lhs.year) < (rhs.month, rhs.year) } - + static let unknown = DateMetadata(title: "#", month: 0, year: 0) - + let title: String let month: Int let year: Int } - + let title: String let items: [SecureVaultItem] - + func withUpdatedItems(_ newItems: [SecureVaultItem]) -> PasswordManagementListSection { return PasswordManagementListSection(title: title, items: newItems) } - + static func sections(with items: [SecureVaultItem], by keyPath: KeyPath, order: SecureVaultSorting.SortOrder) -> [PasswordManagementListSection] { - + let itemsByFirstCharacter: [String: [SecureVaultItem]] = Dictionary(grouping: items) { $0[keyPath: keyPath] } let sortFunction: (String, String) -> Bool = { switch order { @@ -59,36 +59,36 @@ struct PasswordManagementListSection { } }() let sortedKeys = itemsByFirstCharacter.keys.sorted(by: sortFunction) - + return sortedKeys.map { key in var itemsInSection = itemsByFirstCharacter[key] ?? [] itemsInSection.sort { lhs, rhs in sortFunction(lhs.displayTitle, rhs.displayTitle) } return PasswordManagementListSection(title: key, items: itemsInSection) } } - + static func sections(with items: [SecureVaultItem], by keyPath: KeyPath, order: SecureVaultSorting.SortOrder) -> [PasswordManagementListSection] { let itemsByDateMetadata: [DateMetadata: [SecureVaultItem]] = Dictionary(grouping: items) { let date = $0[keyPath: keyPath] - + guard let month = date.components.month, let year = date.components.year else { return DateMetadata.unknown } - + return DateMetadata(title: Self.dateFormatter.string(from: date), month: month, year: year) } - + let metadataSortFunction: (DateMetadata, DateMetadata) -> Bool = order == .ascending ? (>) : (<) let dateSortFunction: (Date, Date) -> Bool = order == .ascending ? (>) : (<) let sortedKeys = itemsByDateMetadata.keys.sorted(by: metadataSortFunction) - + return sortedKeys.map { key in var itemsInSection = itemsByDateMetadata[key] ?? [] itemsInSection.sort { lhs, rhs in dateSortFunction(lhs[keyPath: keyPath], rhs[keyPath: keyPath]) } return PasswordManagementListSection(title: key.title, items: itemsInSection) } } - + } diff --git a/DuckDuckGo/Secure Vault/Model/SecureVaultSorting.swift b/DuckDuckGo/Secure Vault/Model/SecureVaultSorting.swift index 3de939e147..9bb3187f9b 100644 --- a/DuckDuckGo/Secure Vault/Model/SecureVaultSorting.swift +++ b/DuckDuckGo/Secure Vault/Model/SecureVaultSorting.swift @@ -20,17 +20,17 @@ import Foundation import SwiftUI struct SecureVaultSorting: Equatable { - + static let `default` = SecureVaultSorting(category: .allItems, parameter: .title, order: .ascending) enum Category: CaseIterable, Identifiable { var id: Category { self } - + case allItems case logins case identities case cards - + var title: String { switch self { case .allItems: return UserText.passwordManagementAllItems @@ -39,7 +39,7 @@ struct SecureVaultSorting: Equatable { case .cards: return UserText.passwordManagementCreditCards } } - + var imageName: String? { switch self { case .allItems: return nil @@ -48,7 +48,7 @@ struct SecureVaultSorting: Equatable { case .cards: return "CreditCardGlyph" } } - + var backgroundColor: NSColor { switch self { case .allItems: return NSColor(named: "SecureVaultCategoryDefaultColor")! @@ -57,7 +57,7 @@ struct SecureVaultSorting: Equatable { case .cards: return NSColor(named: "CardsColor")! } } - + var foregroundColor: NSColor? { switch self { case .allItems: return nil // Show white or black depending on system appearance @@ -67,12 +67,12 @@ struct SecureVaultSorting: Equatable { } } } - + enum SortParameter: CaseIterable { case title case dateModified case dateCreated - + var title: String { switch self { case .title: return UserText.pmSortParameterTitle @@ -87,11 +87,11 @@ struct SecureVaultSorting: Equatable { } } } - + enum SortOrder: CaseIterable { case ascending case descending - + func title(for sortDataType: SortDataType) -> String { switch sortDataType { case .string: @@ -107,7 +107,7 @@ struct SecureVaultSorting: Equatable { } } } - + enum SortDataType { case string case date diff --git a/DuckDuckGo/Secure Vault/Services/CountryList.swift b/DuckDuckGo/Secure Vault/Services/CountryList.swift index 29d2770209..6f4d4ec0ea 100644 --- a/DuckDuckGo/Secure Vault/Services/CountryList.swift +++ b/DuckDuckGo/Secure Vault/Services/CountryList.swift @@ -23,7 +23,7 @@ struct CountryList { struct Country: Identifiable { let id: String let name: String - + var countryCode: String { return id } diff --git a/DuckDuckGo/Secure Vault/View/NSPathControlView.swift b/DuckDuckGo/Secure Vault/View/NSPathControlView.swift index 02c2c01594..b08a8ead58 100644 --- a/DuckDuckGo/Secure Vault/View/NSPathControlView.swift +++ b/DuckDuckGo/Secure Vault/View/NSPathControlView.swift @@ -21,14 +21,14 @@ import SwiftUI import Combine struct NSPathControlView: NSViewRepresentable { - + typealias NSViewType = NSPathControl var url: URL? - + func makeNSView(context: NSViewRepresentableContext) -> NSPathControl { let newPathControl = NSPathControl() - + newPathControl.wantsLayer = true newPathControl.isEditable = false newPathControl.refusesFirstResponder = true @@ -53,15 +53,15 @@ struct NSPathControlView: NSViewRepresentable { return newPathControl } - + func updateNSView(_ nsView: NSPathControl, context: NSViewRepresentableContext) { nsView.url = url } - + func makeCoordinator() -> Coordinator { return Coordinator() } - + final class Coordinator { var alphaCancellable: AnyCancellable? var borderColorCancellable: AnyCancellable? diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementBitwardenItemView.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementBitwardenItemView.swift index f7376a2fb3..d7e967fbfd 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementBitwardenItemView.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementBitwardenItemView.swift @@ -21,11 +21,11 @@ import SwiftUI struct PasswordManagementBitwardenItemView: View { var manager: PasswordManagerCoordinator let didFinish: () -> Void - + var body: some View { VStack(spacing: 16) { Image("BitwardenLogin") - + VStack(spacing: 2) { Text(UserText.passwordManagerPopoverTitle(managerName: manager.displayName)) HStack (spacing: 3) { @@ -43,7 +43,7 @@ struct PasswordManagementBitwardenItemView: View { .font(.subheadline) .foregroundColor(Color("BlackWhite60")) } - + Button { manager.openPasswordManager() didFinish() diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementIdentityItemView.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementIdentityItemView.swift index 5a98dc46f2..693f3da21a 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementIdentityItemView.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementIdentityItemView.swift @@ -108,70 +108,70 @@ private struct IdentificationView: View { .padding(.bottom, 5) HStack { - + // Way too much code duplication in here, but this view may be altered a fair bit in 2022, and I'm // out of time to fix it up before the end of 2021, so it's staying this way for a bit. Sorry! if Locale.current.dateComponentOrder == .dayMonthYear { NSPopUpButtonView(selection: $model.birthdayDay, viewCreator: { let button = NSPopUpButton() - + let item = button.menu?.addItem(withTitle: UserText.pmDay, action: nil, keyEquivalent: "") item?.representedObject = nil - + for date in Date.daysInMonth { let item = button.menu?.addItem(withTitle: String(date), action: nil, keyEquivalent: "") item?.representedObject = date } - + return button }) NSPopUpButtonView(selection: $model.birthdayMonth, viewCreator: { let button = NSPopUpButton() - + let item = button.menu?.addItem(withTitle: UserText.pmMonth, action: nil, keyEquivalent: "") item?.representedObject = nil - + for date in Date.monthsWithIndex { let item = button.menu?.addItem(withTitle: date.name, action: nil, keyEquivalent: "") item?.representedObject = date.index } - + return button }) } else { NSPopUpButtonView(selection: $model.birthdayMonth, viewCreator: { let button = NSPopUpButton() - + let item = button.menu?.addItem(withTitle: UserText.pmMonth, action: nil, keyEquivalent: "") item?.representedObject = nil - + for date in Date.monthsWithIndex { let item = button.menu?.addItem(withTitle: date.name, action: nil, keyEquivalent: "") item?.representedObject = date.index } - + return button }) NSPopUpButtonView(selection: $model.birthdayDay, viewCreator: { let button = NSPopUpButton() - + let item = button.menu?.addItem(withTitle: UserText.pmDay, action: nil, keyEquivalent: "") item?.representedObject = nil - + for date in Date.daysInMonth { let item = button.menu?.addItem(withTitle: String(date), action: nil, keyEquivalent: "") item?.representedObject = date } - + return button }) } - + NSPopUpButtonView(selection: $model.birthdayYear, viewCreator: { let button = NSPopUpButton() - + let item = button.menu?.addItem(withTitle: UserText.pmYear, action: nil, keyEquivalent: "") item?.representedObject = nil @@ -179,7 +179,7 @@ private struct IdentificationView: View { let item = button.menu?.addItem(withTitle: String(date), action: nil, keyEquivalent: "") item?.representedObject = date } - + return button }) @@ -235,19 +235,19 @@ private struct AddressView: View { NSPopUpButtonView(selection: $model.addressCountryCode, viewCreator: { let button = NSPopUpButton() - + let item = button.menu?.addItem(withTitle: "-", action: nil, keyEquivalent: "") item?.representedObject = "" - + for country in CountryList.countries { let item = button.menu?.addItem(withTitle: country.name, action: nil, keyEquivalent: "") item?.representedObject = country.countryCode } - + return button }) .padding(.bottom, 5) - + } else if !model.addressCountryCode.isEmpty { Text("Country") .bold() diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementItemList.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementItemList.swift index e2d86b5871..a25a48f83a 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementItemList.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementItemList.swift @@ -34,11 +34,11 @@ struct PasswordManagementItemListView: View { private enum Constants { static let dividerFadeInDistance: CGFloat = 100 } - + @EnvironmentObject var model: PasswordManagementItemListModel - + var body: some View { - + VStack(spacing: 0) { PasswordManagementItemListCategoryView() .padding(.top, 15) @@ -46,9 +46,9 @@ struct PasswordManagementItemListView: View { .padding([.leading, .trailing], 10) .disabled(!model.canChangeCategory) .opacity(model.canChangeCategory ? 1.0 : 0.5) - + Divider() - + if #available(macOS 11.0, *) { ScrollView { ScrollViewReader { proxy in @@ -74,34 +74,34 @@ struct PasswordManagementItemListView: View { } struct PasswordManagementItemListCategoryView: View { - + @EnvironmentObject var model: PasswordManagementItemListModel var body: some View { - + HStack(alignment: .center) { - + NSPopUpButtonView(selection: $model.sortDescriptor.category, viewCreator: { let button = PopUpButton() - + for category in SecureVaultSorting.Category.allCases { button.addItem(withTitle: category.title, foregroundColor: category.foregroundColor, backgroundColor: category.backgroundColor) - + if let imageName = category.imageName { button.lastItem?.image = NSImage(named: imageName) } - + button.lastItem?.representedObject = category - + if category == .allItems { button.menu?.addItem(NSMenuItem.separator()) } } - + button.sizeToFit() - + return button }) .alignmentGuide(VerticalAlignment.center) { _ in @@ -110,7 +110,7 @@ struct PasswordManagementItemListCategoryView: View { // to account for it. return 11 } - + Spacer() // MenuButton incorrectly displays a disabled state when you re-render it with a different image. @@ -120,23 +120,23 @@ struct PasswordManagementItemListCategoryView: View { // So, as a last resort, both buttons are kept in a ZStack with their image and opacity is used to determine whether they're visible, which so far seems reliable. // // Reference: https://stackoverflow.com/questions/65602163/swiftui-menu-button-displayed-as-disabled-initially - + if model.sortDescriptor.order == .ascending { PasswordManagementSortButton(imageName: "SortAscending") } else { PasswordManagementSortButton(imageName: "SortDescending") } } - + } } struct PasswordManagementItemListStackView: View { - + @EnvironmentObject var model: PasswordManagementItemListModel - + var body: some View { - + if #available(macOS 11.0, *) { LazyVStack(alignment: .leading) { PasswordManagementItemStackContentsView() @@ -146,14 +146,14 @@ struct PasswordManagementItemListStackView: View { PasswordManagementItemStackContentsView() } } - + } - + } private struct ExternalPasswordManagerItemSection: View { @ObservedObject var model: PasswordManagementItemListModel - + var body: some View { Section(header: Text(UserText.passwordManager).padding(.leading, 18).padding(.top, 0)) { PasswordManagerItemView(model: model) { @@ -165,24 +165,24 @@ private struct ExternalPasswordManagerItemSection: View { } private struct PasswordManagementItemStackContentsView: View { - + @EnvironmentObject var model: PasswordManagementItemListModel private var shouldDisplayExternalPasswordManagerRow: Bool { model.passwordManagerCoordinator.isEnabled && (model.sortDescriptor.category == .allItems || model.sortDescriptor.category == .logins) } - + var body: some View { Spacer(minLength: 10) - + if shouldDisplayExternalPasswordManagerRow { ExternalPasswordManagerItemSection(model: model) } - + ForEach(Array(model.displayedItems.enumerated()), id: \.offset) { index, section in Section(header: Text(section.title).padding(.leading, 18).padding(.top, index == 0 ? 0 : 10)) { - + ForEach(section.items, id: \.id) { item in ItemView(item: item) { model.selected(item: item) @@ -193,26 +193,26 @@ private struct PasswordManagementItemStackContentsView: View { } Spacer(minLength: 10) } - + } private struct PasswordManagerItemView: View { @ObservedObject var model: PasswordManagementItemListModel - + let action: () -> Void - + private var isLocked: Bool { model.passwordManagerCoordinator.isLocked } - + private var lockStatusLabel: String { isLocked ? UserText.passwordManagerLockedStatus : UserText.passwordManagerUnlockedStatus } - + private var selected: Bool { model.externalPasswordManagerSelected } - + var body: some View { let textColor = selected ? .white : Color(NSColor.controlTextColor) let font = Font.custom("SFProText-Regular", size: 13) @@ -221,16 +221,16 @@ private struct PasswordManagerItemView: View { HStack(spacing: 3) { ZStack { Image("BitwardenIcon") - + if isLocked { Image("PasswordManager-lock") .padding(.leading, 28) .padding(.top, 21) } - + }.frame(width: 32) .padding(.leading, 6) - + VStack(alignment: .leading, spacing: 4) { Text(model.passwordManagerCoordinator.displayName) .foregroundColor(textColor) @@ -258,7 +258,7 @@ private struct ItemView: View { let action: () -> Void var body: some View { - + let selected = model.selected == item let textColor = selected ? .white : Color(NSColor.controlTextColor) let font = Font.custom("SFProText-Regular", size: 13) @@ -330,18 +330,18 @@ private struct PasswordManagerItemButtonStyle: ButtonStyle { } struct PasswordManagementSortButton: View { - + @EnvironmentObject var model: PasswordManagementItemListModel @State var sortHover: Bool = false - + let imageName: String - + var body: some View { - + ZStack { Image(imageName) - + // The image is added elsewhere, because MenuButton has a bug with using Images as labels. MenuButton(label: Image(nsImage: NSImage())) { Picker("", selection: $model.sortDescriptor.parameter) { @@ -351,9 +351,9 @@ struct PasswordManagementSortButton: View { } .labelsHidden() .pickerStyle(.radioGroup) - + Divider() - + Picker("", selection: $model.sortDescriptor.order) { ForEach(SecureVaultSorting.SortOrder.allCases, id: \.self) { order in let orderTitle = order.title(for: model.sortDescriptor.parameter.type) @@ -372,9 +372,9 @@ struct PasswordManagementSortButton: View { } .foregroundColor(.red) } - + } - + // The SwiftUI MenuButton view doesn't allow pickers which have checkmarks at the top level; they get put into a submenu. // This title is used in place of a nested picker. private func menuTitle(for string: String, checked: Bool) -> String { @@ -384,5 +384,5 @@ struct PasswordManagementSortButton: View { return " \(string)" } } - + } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementNoteItemView.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementNoteItemView.swift index 53d584def2..9b6da88d03 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementNoteItemView.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementNoteItemView.swift @@ -29,32 +29,32 @@ struct PasswordManagementNoteItemView: View { @EnvironmentObject var model: PasswordManagementNoteModel var body: some View { - + if model.note != nil { - + ZStack(alignment: .top) { Spacer() - + VStack(alignment: .leading, spacing: 0) { - + HeaderView() .padding(.bottom, 30) - + TextView() Spacer(minLength: 0) - + Buttons() - + } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() - + } .padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 10)) - + } - + } } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementPopover.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementPopover.swift index 10cebc0bfb..16b1213e4c 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementPopover.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementPopover.swift @@ -46,7 +46,7 @@ final class PasswordManagementPopover: NSPopover { func select(category: SecureVaultSorting.Category?) { viewController.select(category: category) } - + private func setupContentController() { let controller = PasswordManagementViewController.create() contentViewController = controller @@ -61,7 +61,7 @@ extension PasswordManagementPopover: NSPopoverDelegate { object: nil, queue: OperationQueue.main) { [weak self] _ in guard let self = self, self.isShown else { return } - + if !DeviceAuthenticator.shared.isAuthenticating { self.close() } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift index 2f3ff6380b..9ec8cf7e66 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift @@ -144,7 +144,7 @@ final class PasswordManagementViewController: NSViewController { } private let passwordManagerCoordinator: PasswordManagerCoordinating = PasswordManagerCoordinator.shared - + override func viewDidLoad() { super.viewDidLoad() createListView() @@ -157,7 +157,7 @@ final class PasswordManagementViewController: NSViewController { addVaultItemButton.toolTip = UserText.addItemTooltip moreButton.toolTip = UserText.moreOptionsTooltip - + addVaultItemButton.sendAction(on: .leftMouseDown) moreButton.sendAction(on: .leftMouseDown) @@ -616,7 +616,7 @@ final class PasswordManagementViewController: NSViewController { } var passwordManagerSelectionCancellable: AnyCancellable? - + // swiftlint:disable function_body_length private func createListView() { let listModel = PasswordManagementItemListModel(passwordManagerCoordinator: self.passwordManagerCoordinator) { [weak self] previousValue, newValue in @@ -677,7 +677,7 @@ final class PasswordManagementViewController: NSViewController { self.listModel = listModel self.listView = NSHostingView(rootView: PasswordManagementItemListView().environmentObject(listModel)) - + passwordManagerSelectionCancellable = listModel.$externalPasswordManagerSelected .receive(on: DispatchQueue.main) .removeDuplicates() @@ -687,16 +687,16 @@ final class PasswordManagementViewController: NSViewController { } } } - + private func displayExternalPasswordManagerView() { let passwordManagerView = PasswordManagementBitwardenItemView(manager: PasswordManagerCoordinator.shared) { [weak self] in self?.dismiss() } - + let view = NSHostingView(rootView: passwordManagerView) replaceItemContainerChildView(with: view) } - + // swiftlint:enable function_body_length private func createNewSecureVaultItemMenu() -> NSMenu { diff --git a/DuckDuckGo/Secure Vault/View/PopUpButton.swift b/DuckDuckGo/Secure Vault/View/PopUpButton.swift index f2f18cbee5..a77831daa8 100644 --- a/DuckDuckGo/Secure Vault/View/PopUpButton.swift +++ b/DuckDuckGo/Secure Vault/View/PopUpButton.swift @@ -24,42 +24,42 @@ private struct NSMenuItemColor { } final class PopUpButton: NSPopUpButton { - + var backgroundColorCell: NSPopUpButtonBackgroundColorCell? { return self.cell as? NSPopUpButtonBackgroundColorCell } - + init() { super.init(frame: .zero, pullsDown: true) self.cell = NSPopUpButtonBackgroundColorCell() } - + override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.cell = NSPopUpButtonBackgroundColorCell() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func addItem(withTitle title: String, foregroundColor: NSColor?, backgroundColor: NSColor) { self.addItem(withTitle: title) - + let itemColor = NSMenuItemColor(foregroundColor: foregroundColor, backgroundColor: backgroundColor) backgroundColorCell?.colors[title] = itemColor } - + } final class NSPopUpButtonBackgroundColorCell: NSPopUpButtonCell { private static let chevronsImage = NSImage(named: "PopUpButtonChevrons")! - + fileprivate var colors: [String: NSMenuItemColor] = [:] - + private func foregroundColor(for title: String) -> NSColor { if let color = colors[title]?.foregroundColor { return color @@ -67,7 +67,7 @@ final class NSPopUpButtonBackgroundColorCell: NSPopUpButtonCell { return NSApplication.shared.effectiveAppearance.name == .aqua ? .black : .white } } - + override func drawTitle(withFrame cellFrame: NSRect, in controlView: NSView) { let font = self.font ?? NSFont.systemFont(ofSize: 15) @@ -80,7 +80,7 @@ final class NSPopUpButtonBackgroundColorCell: NSPopUpButtonCell { titleRect.origin.y += 2 string.draw(in: titleRect) } - + override func drawImage(withFrame cellFrame: NSRect, in controlView: NSView) { let color = foregroundColor(for: title) guard let tintedImage = image?.tinted(with: color) else { @@ -89,10 +89,10 @@ final class NSPopUpButtonBackgroundColorCell: NSPopUpButtonCell { var imageRect = imageRect(forBounds: cellFrame) imageRect.origin.y += 1 - + tintedImage.draw(in: imageRect) } - + override func drawBezel(withFrame frame: NSRect, in controlView: NSView) { guard let color = colors[title] else { return @@ -103,14 +103,14 @@ final class NSPopUpButtonBackgroundColorCell: NSPopUpButtonCell { modifiedFrame.origin.x += horizontalOffset modifiedFrame.size.width -= horizontalOffset modifiedFrame.size.height -= 1 - + color.backgroundColor.setFill() let backgroundPath = NSBezierPath(roundedRect: modifiedFrame, xRadius: 5, yRadius: 5) backgroundPath.fill() - + let foregroundColor = foregroundColor(for: title) - + let tintedChevrons = Self.chevronsImage.tinted(with: foregroundColor) let chevronFrame = NSRect(x: frame.size.width - Self.chevronsImage.size.width - 4, y: frame.size.height / 2 - Self.chevronsImage.size.height / 2, diff --git a/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift b/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift index a4fb496446..a4f7e28618 100644 --- a/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift @@ -47,7 +47,7 @@ final class SaveCredentialsViewController: NSViewController { @IBOutlet var usernameField: NSTextField! @IBOutlet var hiddenPasswordField: NSSecureTextField! @IBOutlet var visiblePasswordField: NSTextField! - + @IBOutlet var notNowButton: NSButton! @IBOutlet var saveButton: NSButton! @IBOutlet var updateButton: NSButton! @@ -96,7 +96,7 @@ final class SaveCredentialsViewController: NSViewController { override func viewWillDisappear() { passwordManagerStateCancellable = nil } - + /// Note that if the credentials.account.id is not nil, then we consider this an update rather than a save. func update(credentials: SecureVaultModels.WebsiteCredentials, automaticallySaved: Bool) { self.credentials = credentials @@ -105,14 +105,14 @@ final class SaveCredentialsViewController: NSViewController { self.hiddenPasswordField.stringValue = String(data: credentials.password, encoding: .utf8) ?? "" self.visiblePasswordField.stringValue = self.hiddenPasswordField.stringValue self.loadFaviconForDomain(credentials.account.domain) - + fireproofCheck.state = FireproofDomains.shared.isFireproof(fireproofDomain: credentials.account.domain) ? .on : .off - + // Only use the non-editable state if a credential was automatically saved and it didn't already exist. let condition = credentials.account.id != nil && !credentials.account.username.isEmpty && automaticallySaved updateViewState(editable: !condition) } - + private func updateViewState(editable: Bool) { usernameField.setEditable(editable) hiddenPasswordField.setEditable(editable) @@ -131,7 +131,7 @@ final class SaveCredentialsViewController: NSViewController { titleLabel.isHidden = passwordManagerCoordinator.isEnabled passwordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || passwordManagerCoordinator.isLocked - passwordManagerAccountLabel.stringValue = "Connected to \(passwordManagerCoordinator.activeVaultEmail ?? "")" + passwordManagerAccountLabel.stringValue = "Connected to \(passwordManagerCoordinator.activeVaultEmail ?? "")" unlockPasswordManagerTitle.isHidden = !passwordManagerCoordinator.isEnabled || !passwordManagerCoordinator.isLocked titleLabel.stringValue = UserText.pmSaveCredentialsEditableTitle usernameField.makeMeFirstResponder() @@ -228,11 +228,11 @@ final class SaveCredentialsViewController: NSViewController { @IBAction func onOpenPasswordManagerClicked(sender: Any?) { passwordManagerCoordinator.openPasswordManager() } - + @IBAction func onEditClicked(sender: Any?) { updateViewState(editable: true) } - + @IBAction func onDoneClicked(sender: Any?) { delegate?.shouldCloseSaveCredentialsViewController(self) } diff --git a/DuckDuckGo/Secure Vault/View/SaveIdentityViewController.swift b/DuckDuckGo/Secure Vault/View/SaveIdentityViewController.swift index 7a2164ee43..4609cf5e52 100644 --- a/DuckDuckGo/Secure Vault/View/SaveIdentityViewController.swift +++ b/DuckDuckGo/Secure Vault/View/SaveIdentityViewController.swift @@ -28,7 +28,7 @@ protocol SaveIdentityDelegate: AnyObject { } final class SaveIdentityViewController: NSViewController { - + enum Constants { static let storyboardName = "PasswordManager" static let identifier = "SaveIdentity" @@ -41,32 +41,32 @@ final class SaveIdentityViewController: NSViewController { return controller } - + @IBOutlet private var identityStackView: NSStackView! - + weak var delegate: SaveIdentityDelegate? - + private var identity: SecureVaultModels.Identity? private var appearanceCancellable: AnyCancellable? // MARK: - Actions - + @IBAction func onNotNowClicked(sender: NSButton) { self.delegate?.shouldCloseSaveIdentityViewController(self) } - + @IBAction func onSaveClicked(sender: NSButton) { defer { self.delegate?.shouldCloseSaveIdentityViewController(self) } - + guard var identity = identity else { assertionFailure("Tried to save identity, but the view controller didn't have one") return } - + identity.title = UserText.pmDefaultIdentityAutofillTitle - + do { try SecureVaultFactory.default.makeVault(errorReporter: SecureVaultErrorReporter.shared).storeIdentity(identity) Pixel.fire(.autofillItemSaved(kind: .identity)) @@ -74,17 +74,17 @@ final class SaveIdentityViewController: NSViewController { os_log("%s:%s: failed to store identity %s", type: .error, className, #function, error.localizedDescription) } } - + @IBAction func onOpenPreferencesClicked(sender: NSButton) { WindowControllersManager.shared.showPreferencesTab() self.delegate?.shouldCloseSaveIdentityViewController(self) } - + // MARK: - Public - + func saveIdentity(_ identity: SecureVaultModels.Identity) { self.identity = identity - + buildStackView(from: identity) } @@ -95,9 +95,9 @@ final class SaveIdentityViewController: NSViewController { appearanceCancellable = view.subscribeForAppApperanceUpdates() } - + // MARK: - Private - + private func buildStackView(from identity: SecureVaultModels.Identity) { // Placeholder views are used in the Storyboard, which need to be removed before laying out the correct views. @@ -106,23 +106,23 @@ final class SaveIdentityViewController: NSViewController { } identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.longFormattedName)) - + identityStackView.setCustomSpacingAfterLastView(20) - + identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.addressStreet)) identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.addressStreet2)) identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.addressCity)) identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.addressProvince)) identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.addressPostalCode)) identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.addressCountryCode)) - + identityStackView.setCustomSpacingAfterLastView(20) - + identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.homePhone)) identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.mobilePhone)) - + identityStackView.setCustomSpacingAfterLastView(20) - + identityStackView.addArrangedSubview(NSTextField.optionalLabel(titled: identity.emailAddress)) } diff --git a/DuckDuckGo/Secure Vault/View/SavePaymentMethodViewController.swift b/DuckDuckGo/Secure Vault/View/SavePaymentMethodViewController.swift index 5cb0637c05..df35621d38 100644 --- a/DuckDuckGo/Secure Vault/View/SavePaymentMethodViewController.swift +++ b/DuckDuckGo/Secure Vault/View/SavePaymentMethodViewController.swift @@ -28,7 +28,7 @@ protocol SavePaymentMethodDelegate: AnyObject { } final class SavePaymentMethodViewController: NSViewController { - + enum Constants { static let storyboardName = "PasswordManager" static let identifier = "SavePaymentMethod" @@ -41,46 +41,46 @@ final class SavePaymentMethodViewController: NSViewController { return controller } - + @IBOutlet var cardDetailsLabel: NSTextField! @IBOutlet var cardExpirationLabel: NSTextField! - + weak var delegate: SavePaymentMethodDelegate? - + private var paymentMethod: SecureVaultModels.CreditCard? private var appearanceCancellable: AnyCancellable? // MARK: - Public - + func savePaymentMethod(_ paymentMethod: SecureVaultModels.CreditCard) { self.paymentMethod = paymentMethod - + let type = CreditCardValidation.type(for: paymentMethod.cardNumber) cardDetailsLabel.stringValue = "\(type.displayName) ••••\(paymentMethod.cardSuffix)" - + if let expirationMonth = paymentMethod.expirationMonth, let expirationYear = paymentMethod.expirationYear { cardExpirationLabel.stringValue = "\(expirationMonth)/\(expirationYear)" } else { cardExpirationLabel.stringValue = "" } } - + // MARK: - Actions - + @IBAction func onNotNowClicked(sender: NSButton) { self.delegate?.shouldCloseSavePaymentMethodViewController(self) } - + @IBAction func onSaveClicked(sender: NSButton) { defer { self.delegate?.shouldCloseSavePaymentMethodViewController(self) } - + guard var paymentMethod = paymentMethod else { assertionFailure("Tried to save payment method, but the view controller didn't have one") return } - + paymentMethod.title = CreditCardValidation.type(for: paymentMethod.cardNumber).displayName do { @@ -89,7 +89,7 @@ final class SavePaymentMethodViewController: NSViewController { os_log("%s:%s: failed to store payment method %s", type: .error, className, #function, error.localizedDescription) } } - + @IBAction func onOpenPreferencesClicked(sender: NSButton) { WindowControllersManager.shared.showPreferencesTab() self.delegate?.shouldCloseSavePaymentMethodViewController(self) diff --git a/DuckDuckGo/Smarter Encryption/HTTPSBloomFilterSpecification.swift b/DuckDuckGo/Smarter Encryption/HTTPSBloomFilterSpecification.swift index 6d9a51beb0..218e535384 100644 --- a/DuckDuckGo/Smarter Encryption/HTTPSBloomFilterSpecification.swift +++ b/DuckDuckGo/Smarter Encryption/HTTPSBloomFilterSpecification.swift @@ -19,7 +19,7 @@ import BrowserServicesKit extension HTTPSBloomFilterSpecification { - + static func copy(storedSpecification specification: HTTPSStoredBloomFilterSpecification?) -> HTTPSBloomFilterSpecification? { guard let specification = specification else { return nil } return HTTPSBloomFilterSpecification(bitCount: Int(specification.bitCount), @@ -27,5 +27,5 @@ extension HTTPSBloomFilterSpecification { totalEntries: Int(specification.totalEntries), sha256: specification.sha256!) } - + } diff --git a/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift b/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift index 3a73ddb380..ffe8784763 100644 --- a/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift +++ b/DuckDuckGo/Smarter Encryption/HTTPSUpgrade.swift @@ -23,7 +23,7 @@ import BrowserServicesKit final class HTTPSUpgrade { static let shared = HTTPSUpgrade() - + private let dataReloadLock = NSLock() private let store: HTTPSUpgradeStore private var bloomFilter: BloomFilterWrapper? @@ -33,15 +33,15 @@ final class HTTPSUpgrade { } func isUpgradeable(url: URL, config: PrivacyConfiguration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig) -> Bool { - + guard url.scheme == URL.NavigationalScheme.http.rawValue else { return false } - + guard let host = url.host else { return false } - + if store.shouldExcludeDomain(host) { return false } @@ -49,30 +49,30 @@ final class HTTPSUpgrade { guard config.isFeature(.httpsUpgrade, enabledForDomain: host) else { return false } - + waitForAnyReloadsToComplete() let isUpgradable = isInUpgradeList(host: host) return isUpgradable } - + private func isInUpgradeList(host: String) -> Bool { guard let bloomFilter = bloomFilter else { return false } return bloomFilter.contains(host) } - + private func waitForAnyReloadsToComplete() { // wait for lock (by locking and unlocking) before continuing dataReloadLock.lock() dataReloadLock.unlock() } - + func loadDataAsync() { DispatchQueue.global(qos: .background).async { self.loadData() } } - + func loadData() { if !dataReloadLock.try() { os_log("Reload already in progress", type: .debug) diff --git a/DuckDuckGo/Smarter Encryption/Store/AppHTTPSUpgradeStore.swift b/DuckDuckGo/Smarter Encryption/Store/AppHTTPSUpgradeStore.swift index bf0f739bcf..2e24b81e28 100644 --- a/DuckDuckGo/Smarter Encryption/Store/AppHTTPSUpgradeStore.swift +++ b/DuckDuckGo/Smarter Encryption/Store/AppHTTPSUpgradeStore.swift @@ -25,31 +25,31 @@ extension HTTPSStoredBloomFilterSpecification: Managed {} extension HTTPSExcludedDomain: Managed {} public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { - + private struct Resource { static var bloomFilter: URL { return URL.sandboxApplicationSupportURL.appendingPathComponent("HttpsBloomFilter.bin") } } - + private struct EmbeddedResource { static let bloomSpecification = Bundle.main.url(forResource: "httpsMobileV2BloomSpec", withExtension: "json")! static let bloomFilter = Bundle.main.url(forResource: "httpsMobileV2Bloom", withExtension: "bin")! static let excludedDomains = Bundle.main.url(forResource: "httpsMobileV2FalsePositives", withExtension: "json")! } - + private struct EmbeddedBloomData { let specification: HTTPSBloomFilterSpecification let bloomFilter: Data let excludedDomains: [String] } - + private let context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "HTTPSUpgrade") - + private var hasBloomFilterData: Bool { return (try? Resource.bloomFilter.checkResourceIsReachable()) ?? false } - + public var bloomFilter: BloomFilterWrapper? { let storedSpecification = hasBloomFilterData ? bloomFilterSpecification : loadEmbeddedData()?.specification guard let specification = storedSpecification else { return nil } @@ -57,7 +57,7 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { withBitCount: Int32(specification.bitCount), andTotalItems: Int32(specification.totalEntries)) } - + public var bloomFilterSpecification: HTTPSBloomFilterSpecification? { var specification: HTTPSBloomFilterSpecification? context.performAndWait { @@ -69,7 +69,7 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { } return specification ?? loadEmbeddedData()?.specification } - + private func loadEmbeddedData() -> EmbeddedBloomData? { os_log("Loading embedded https data") guard let specificationData = try? Data(contentsOf: EmbeddedResource.bloomSpecification), @@ -84,14 +84,14 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { persistExcludedDomains(excludedDomains.data) return EmbeddedBloomData(specification: specification, bloomFilter: bloomData, excludedDomains: excludedDomains.data) } - + @discardableResult func persistBloomFilter(specification: HTTPSBloomFilterSpecification, data: Data) -> Bool { guard data.sha256 == specification.sha256 else { return false } guard persistBloomFilter(data: data) else { return false } persistBloomFilterSpecification(specification) return true } - + private func persistBloomFilter(data: Data) -> Bool { do { try data.write(to: Resource.bloomFilter, options: .atomic) @@ -100,22 +100,22 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { return false } } - + private func deleteBloomFilter() { try? FileManager.default.removeItem(at: Resource.bloomFilter) } - + func persistBloomFilterSpecification(_ specification: HTTPSBloomFilterSpecification) { - + context.performAndWait { deleteBloomFilterSpecification() - + let storedEntity: HTTPSStoredBloomFilterSpecification = context.insertObject() storedEntity.bitCount = Int64(specification.bitCount) storedEntity.totalEntries = Int64(specification.totalEntries) storedEntity.errorRate = specification.errorRate storedEntity.sha256 = specification.sha256 - + do { try context.save() } catch { @@ -123,13 +123,13 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { } } } - + private func deleteBloomFilterSpecification() { context.performAndWait { context.deleteAll(matching: HTTPSStoredBloomFilterSpecification.fetchRequest()) } } - + public func hasExcludedDomain(_ domain: String) -> Bool { var result = false context.performAndWait { @@ -140,12 +140,12 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { } return result } - + @discardableResult public func persistExcludedDomains(_ domains: [String]) -> Bool { var result = true context.performAndWait { deleteExcludedDomains() - + for domain in domains { let storedDomain: HTTPSExcludedDomain = context.insertObject() storedDomain.domain = domain.lowercased() @@ -159,13 +159,13 @@ public final class AppHTTPSUpgradeStore: HTTPSUpgradeStore { } return result } - + private func deleteExcludedDomains() { context.performAndWait { context.deleteAll(matching: HTTPSExcludedDomain.fetchRequest()) } } - + func reset() { deleteBloomFilterSpecification() deleteBloomFilter() diff --git a/DuckDuckGo/Statistics/ATB/AtbAndVariantCleanup.swift b/DuckDuckGo/Statistics/ATB/AtbAndVariantCleanup.swift index 7807a42888..8f019f5234 100644 --- a/DuckDuckGo/Statistics/ATB/AtbAndVariantCleanup.swift +++ b/DuckDuckGo/Statistics/ATB/AtbAndVariantCleanup.swift @@ -22,14 +22,14 @@ public class AtbAndVariantCleanup { static func cleanup(statisticsStorage: StatisticsStore = LocalStatisticsStore(), variantManager: VariantManager = DefaultVariantManager()) { - + guard let variant = statisticsStorage.variant else { return } // clean up ATB if let atb = statisticsStorage.atb, atb.hasSuffix(variant) { statisticsStorage.atb = String(atb.dropLast(variant.count)) } - + // remove existing variant if not in an active experiment if variantManager.currentVariant == nil { statisticsStorage.variant = nil diff --git a/DuckDuckGo/Statistics/ATB/LocalStatisticsStore.swift b/DuckDuckGo/Statistics/ATB/LocalStatisticsStore.swift index 3842682929..4885a848c6 100644 --- a/DuckDuckGo/Statistics/ATB/LocalStatisticsStore.swift +++ b/DuckDuckGo/Statistics/ATB/LocalStatisticsStore.swift @@ -35,7 +35,7 @@ final class LocalStatisticsStore: StatisticsStore { @UserDefaultsWrapper(key: .lastAppRetentionRequestDate, defaultValue: nil) var lastAppRetentionRequestDate: Date? - + /// Used to determine whether this clearing process took place. While we no longer use these values, we need to know if a user has upgraded from a /// version which did use them, so that they can be shephered into an unlocked waitlist state. When the waitlist feature is removed, this key can be deleted. @UserDefaultsWrapper(key: .legacyStatisticsStoreDataCleared, defaultValue: false) @@ -89,7 +89,7 @@ final class LocalStatisticsStore: StatisticsStore { var hasInstallStatistics: Bool { return atb != nil } - + /// There are three cases in which users can upgrade to a version which includes the Lock Screen feature: /// /// 1. Users with ATB stored in User Defaults @@ -101,7 +101,7 @@ final class LocalStatisticsStore: StatisticsStore { let legacyATBWasMigrated = LegacyStatisticsStore().legacyStatisticsStoreDataCleared let deprecatedATB: String? = pixelDataStore.value(forKey: DeprecatedKeys.atb) let hasDeprecatedATB = deprecatedATB != nil - + return hasInstallStatistics || hasDeprecatedATB || legacyATBWasMigrated } @@ -186,7 +186,7 @@ final class LocalStatisticsStore: StatisticsStore { } } } - + var waitlistUnlocked: Bool { get { guard let booleanStringValue: String = pixelDataStore.value(forKey: Keys.waitlistUnlocked) else { return false } @@ -203,7 +203,7 @@ final class LocalStatisticsStore: StatisticsStore { } } } - + var autoLockEnabled: Bool { get { guard let booleanStringValue: String = pixelDataStore.value(forKey: Keys.autoLockEnabled) else { @@ -216,7 +216,7 @@ final class LocalStatisticsStore: StatisticsStore { pixelDataStore.set(booleanAsString, forKey: Keys.autoLockEnabled) } } - + var autoLockThreshold: String? { get { pixelDataStore.value(forKey: Keys.autoLockThreshold) diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index 952d741a8c..c8646e4bb2 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -20,15 +20,15 @@ import Foundation import os.log final class StatisticsLoader { - + typealias Completion = (() -> Void) - + static let shared = StatisticsLoader() - + private let statisticsStore: StatisticsStore private let parser = AtbParser() private var isAppRetentionRequestInProgress = false - + init(statisticsStore: StatisticsStore = LocalStatisticsStore()) { self.statisticsStore = statisticsStore } @@ -66,7 +66,7 @@ final class StatisticsLoader { requestInstallStatistics(completion: completion) } - + private func requestInstallStatistics(completion: @escaping Completion = {}) { dispatchPrecondition(condition: .onQueue(.main)) @@ -74,7 +74,7 @@ final class StatisticsLoader { isAppRetentionRequestInProgress = true os_log("Requesting install statistics", log: .atb, type: .debug) - + APIRequest.request(url: URL.initialAtb) { response, error in DispatchQueue.main.async { self.isAppRetentionRequestInProgress = false @@ -85,7 +85,7 @@ final class StatisticsLoader { } os_log("Install statistics request succeeded", log: .atb, type: .debug) - + if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.requestExti(atb: atb, completion: completion) } else { @@ -94,7 +94,7 @@ final class StatisticsLoader { } } } - + private func requestExti(atb: Atb, completion: @escaping Completion = {}) { dispatchPrecondition(condition: .onQueue(.main)) @@ -102,7 +102,7 @@ final class StatisticsLoader { self.isAppRetentionRequestInProgress = true os_log("Requesting exti", log: .atb, type: .debug) - + let installAtb = atb.version + (statisticsStore.variant ?? "") let url = URL.exti(forAtb: installAtb) APIRequest.request(url: url) { _, error in @@ -113,7 +113,7 @@ final class StatisticsLoader { completion() return } - + os_log("Exti request succeeded", log: .atb, type: .debug) assert(self.statisticsStore.atb == nil) @@ -125,7 +125,7 @@ final class StatisticsLoader { } } } - + func refreshSearchRetentionAtb(completion: @escaping Completion = {}) { dispatchPrecondition(condition: .onQueue(.main)) @@ -135,7 +135,7 @@ final class StatisticsLoader { requestInstallStatistics(completion: completion) return } - + os_log("Requesting search retention ATB", log: .atb, type: .debug) let url = URL.searchAtb(atbWithVariant: atbWithVariant, setAtb: searchRetentionAtb) @@ -146,19 +146,19 @@ final class StatisticsLoader { completion() return } - + os_log("Search retention ATB request succeeded", log: .atb, type: .debug) - + if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.statisticsStore.searchRetentionAtb = atb.version self.storeUpdateVersionIfPresent(atb) } - + completion() } } } - + func refreshAppRetentionAtb(completion: @escaping Completion = {}) { dispatchPrecondition(condition: .onQueue(.main)) @@ -171,9 +171,9 @@ final class StatisticsLoader { } os_log("Requesting app retention ATB", log: .atb, type: .debug) - + isAppRetentionRequestInProgress = true - + let url = URL.appRetentionAtb(atbWithVariant: atbWithVariant, setAtb: appRetentionAtb) APIRequest.request(url: url) { response, error in DispatchQueue.main.async { @@ -186,7 +186,7 @@ final class StatisticsLoader { } os_log("App retention ATB request succeeded", log: .atb, type: .debug) - + if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.statisticsStore.appRetentionAtb = atb.version self.statisticsStore.lastAppRetentionRequestDate = Date() @@ -200,7 +200,7 @@ final class StatisticsLoader { func storeUpdateVersionIfPresent(_ atb: Atb) { dispatchPrecondition(condition: .onQueue(.main)) - + if let updateVersion = atb.updateVersion { statisticsStore.atb = updateVersion } diff --git a/DuckDuckGo/Statistics/ATB/StatisticsStore.swift b/DuckDuckGo/Statistics/ATB/StatisticsStore.swift index d2d9c1dee4..ec9782b7b9 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsStore.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsStore.swift @@ -29,12 +29,12 @@ protocol StatisticsStore: AnyObject { var lastAppRetentionRequestDate: Date? { get set } var isAppRetentionFiredToday: Bool { get } - + var waitlistUnlocked: Bool { get set } var autoLockEnabled: Bool { get set } var autoLockThreshold: String? { get set } - + } extension StatisticsStore { @@ -51,5 +51,5 @@ extension StatisticsStore { var isAppRetentionFiredToday: Bool { Date.startOfDayToday == lastAppRetentionRequestDate.map(Calendar.current.startOfDay) } - + } diff --git a/DuckDuckGo/Statistics/PixelArguments.swift b/DuckDuckGo/Statistics/PixelArguments.swift index 93549e15a2..53e88aaaa7 100644 --- a/DuckDuckGo/Statistics/PixelArguments.swift +++ b/DuckDuckGo/Statistics/PixelArguments.swift @@ -284,7 +284,7 @@ extension Pixel.Event { case importLogins = "logins" case generic = "generic" } - + enum DataImportSource: String, CustomStringConvertible { var description: String { rawValue } @@ -300,14 +300,14 @@ extension Pixel.Event { } public enum CompileRulesListType: String, CustomStringConvertible { - + public var description: String { rawValue } - + case tds = "tracker_data" case clickToLoad = "click_to_load" case blockingAttribution = "blocking_attribution" case attributed = "attributed" case unknown = "unknown" - + } } diff --git a/DuckDuckGo/Statistics/PixelDataStore.swift b/DuckDuckGo/Statistics/PixelDataStore.swift index c18d769982..c500f2c48a 100644 --- a/DuckDuckGo/Statistics/PixelDataStore.swift +++ b/DuckDuckGo/Statistics/PixelDataStore.swift @@ -31,7 +31,7 @@ protocol PixelDataStore { func set(_ value: String, forKey: String, completionHandler: ((Error?) -> Void)?) func removeValue(forKey key: String, completionHandler: ((Error?) -> Void)?) - + } extension PixelDataStore { @@ -188,7 +188,7 @@ final class LocalPixelDataStore: PixelDataStore { let deletedObjects = result?.result as? [NSManagedObjectID] ?? [] let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: deletedObjects] NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) - + mainQueueCompletion(nil) } catch { mainQueueCompletion(error) diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 7eaeae5f50..364906178a 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -89,7 +89,7 @@ extension Pixel { } case serp - + case dataImportFailed(action: DataImportAction, source: DataImportSource) case faviconImportFailed(source: DataImportSource) @@ -98,14 +98,14 @@ extension Pixel { case autoconsentOptOutFailed case autoconsentSelfTestFailed - + case ampBlockingRulesCompilationFailed - + case adClickAttributionDetected case adClickAttributionActive - + case jsPixel(_ pixel: AutofillUserScript.JSPixel) - + case debug(event: Debug, error: Error? = nil) enum Debug { @@ -134,7 +134,7 @@ extension Pixel { case appStateRestorationFailed case contentBlockingErrorReportingIssue - + case contentBlockingCompilationFailed(listType: CompileRulesListType, component: ContentBlockerDebugEvents.Component) @@ -170,9 +170,9 @@ extension Pixel { case adAttributionLogicRequestingAttributionTimedOut case adAttributionLogicWrongVendorOnSuccessfulCompilation case adAttributionLogicWrongVendorOnFailedCompilation - + case webKitDidTerminate - + case removedInvalidBookmarkManagedObjects case bitwardenNotResponding @@ -212,13 +212,13 @@ extension Pixel.Event { case .compileRulesWait(onboardingShown: let onboardingShown, waitTime: let waitTime, result: let result): return "m_mac_cbr-wait_\(onboardingShown)_\(waitTime)_\(result)" - + case .serp: return "m_mac_navigation_search" case .dataImportFailed(action: let action, source: let source): return "m_mac_data-import-failed_\(action)_\(source)" - + case .faviconImportFailed(source: let source): return "m_mac_favicon-import-failed_\(source)" @@ -236,16 +236,16 @@ extension Pixel.Event { case .autoconsentSelfTestFailed: return "m_mac_autoconsent_selftest_failed" - + case .ampBlockingRulesCompilationFailed: return "m_mac_amp_rules_compilation_failed" - + case .adClickAttributionDetected: return "m_mac_ad_click_detected" - + case .adClickAttributionActive: return "m_mac_ad_click_active" - + case .jsPixel(pixel: let pixel): return "m_mac_\(pixel.pixelName)" } @@ -253,7 +253,7 @@ extension Pixel.Event { } extension Pixel.Event.Debug { - + var name: String { switch self { @@ -267,39 +267,39 @@ extension Pixel.Event.Debug { return "dbsw" case .dbSaveBloomFilterError: return "dbsb" - + case .configurationFetchError: return "cfgfetch" - + case .trackerDataParseFailed: return "tds_p" case .trackerDataReloadFailed: return "tds_r" case .trackerDataCouldNotBeLoaded: return "tds_l" - + case .privacyConfigurationParseFailed: return "pcf_p" case .privacyConfigurationReloadFailed: return "pcf_r" case .privacyConfigurationCouldNotBeLoaded: return "pcf_l" - + case .fileStoreWriteFailed: return "fswf" case .fileMoveToDownloadsFailed: return "df" - + case .suggestionsFetchFailed: return "sgf" case .appOpenURLFailed: return "url" case .appStateRestorationFailed: return "srf" - + case .contentBlockingErrorReportingIssue: return "content_blocking_error_reporting_issue" - + case .contentBlockingCompilationFailed(let listType, let component): let componentString: String switch component { @@ -315,21 +315,21 @@ extension Pixel.Event.Debug { componentString = "fallback_tds" } return "content_blocking_\(listType)_compilation_error_\(componentString)" - + case .contentBlockingCompilationTime: return "content_blocking_compilation_time" - + case .secureVaultInitError: return "secure_vault_init_error" case .secureVaultError: return "secure_vault_error" - + case .feedbackReportingFailed: return "feedback_reporting_failed" - + case .blankNavigationOnBurnFailed: return "blank_navigation_on_burn_failed" - + case .historyRemoveFailed: return "history_remove_failed" case .historyReloadFailed: @@ -344,15 +344,15 @@ extension Pixel.Event.Debug { return "history_insert_visit_failed" case .historyRemoveVisitsFailed: return "history_remove_visits_failed" - + case .emailAutofillKeychainError: return "email_autofill_keychain_error" - + case .bookmarksStoreRootFolderMigrationFailed: return "bookmarks_store_root_folder_migration_failed" case .bookmarksStoreFavoritesFolderMigrationFailed: return "bookmarks_store_favorites_folder_migration_failed" - + case .adAttributionCompilationFailedForAttributedRulesList: return "ad_attribution_compilation_failed_for_attributed_rules_list" case .adAttributionGlobalAttributedRulesDoNotExist: @@ -373,10 +373,10 @@ extension Pixel.Event.Debug { return "ad_attribution_logic_wrong_vendor_on_successful_compilation" case .adAttributionLogicWrongVendorOnFailedCompilation: return "ad_attribution_logic_wrong_vendor_on_failed_compilation" - + case .webKitDidTerminate: return "webkit_did_terminate" - + case .removedInvalidBookmarkManagedObjects: return "removed_invalid_bookmark_managed_objects" diff --git a/DuckDuckGo/Statistics/PixelParameters.swift b/DuckDuckGo/Statistics/PixelParameters.swift index 2b41aeb11b..042cddda65 100644 --- a/DuckDuckGo/Statistics/PixelParameters.swift +++ b/DuckDuckGo/Statistics/PixelParameters.swift @@ -80,7 +80,7 @@ extension Pixel.Event { .adClickAttributionDetected, .adClickAttributionActive, .jsPixel: - + return nil } } diff --git a/DuckDuckGo/Statistics/RulesCompilationMonitor.swift b/DuckDuckGo/Statistics/RulesCompilationMonitor.swift index a13f9c4975..86e704f4d7 100644 --- a/DuckDuckGo/Statistics/RulesCompilationMonitor.swift +++ b/DuckDuckGo/Statistics/RulesCompilationMonitor.swift @@ -58,7 +58,7 @@ final class AbstractContentBlockingAssetsCompilationTimeReporter SuggestionViewModel? { let items = suggestionContainer.result?.all ?? [] @@ -165,5 +165,5 @@ final class SuggestionContainerViewModel { let newIndex = max(0, selectionIndex - 1) select(at: newIndex) } - + } diff --git a/DuckDuckGo/Tab Bar/View/TabBarCollectionView.swift b/DuckDuckGo/Tab Bar/View/TabBarCollectionView.swift index 9d4e8dc569..9533218e3a 100644 --- a/DuckDuckGo/Tab Bar/View/TabBarCollectionView.swift +++ b/DuckDuckGo/Tab Bar/View/TabBarCollectionView.swift @@ -24,10 +24,10 @@ final class TabBarCollectionView: NSCollectionView { override var acceptsFirstResponder: Bool { return false } - + override func awakeFromNib() { super.awakeFromNib() - + let nib = NSNib(nibNamed: "TabBarViewItem", bundle: nil) register(nib, forItemWithIdentifier: TabBarViewItem.identifier) @@ -50,7 +50,7 @@ final class TabBarCollectionView: NSCollectionView { deselectItems(at: selectionIndexPaths) } } - + func scrollToSelected() { guard selectionIndexPaths.count == 1, let indexPath = selectionIndexPaths.first else { os_log("TabBarCollectionView: More than 1 item or no item highlighted", type: .error) @@ -62,7 +62,7 @@ final class TabBarCollectionView: NSCollectionView { animator().scrollToVisible(rect) }, completionHandler: nil) } - + func scrollToEnd(completionHandler: ((Bool) -> Void)? = nil) { animator().performBatchUpdates({ animator().scroll(CGPoint(x: self.bounds.size.width, y: 0)) diff --git a/DuckDuckGo/Tab Bar/View/TabBarScrollView.swift b/DuckDuckGo/Tab Bar/View/TabBarScrollView.swift index 591d53f739..76e066858f 100644 --- a/DuckDuckGo/Tab Bar/View/TabBarScrollView.swift +++ b/DuckDuckGo/Tab Bar/View/TabBarScrollView.swift @@ -25,7 +25,7 @@ final class TabBarScrollView: NSScrollView { } // Hiding scrollers in storyboard doesn't work. It's a known bug - + override var hasHorizontalScroller: Bool { get { return false } set { super.hasHorizontalScroller = newValue } diff --git a/DuckDuckGo/Tab Bar/View/TabBarViewController.swift b/DuckDuckGo/Tab Bar/View/TabBarViewController.swift index 9614866990..6c52e35738 100644 --- a/DuckDuckGo/Tab Bar/View/TabBarViewController.swift +++ b/DuckDuckGo/Tab Bar/View/TabBarViewController.swift @@ -56,7 +56,7 @@ final class TabBarViewController: NSViewController { private var cancellables = Set() @IBOutlet weak var shadowView: TabShadowView! - + @IBOutlet weak var rightSideStackView: NSStackView! var footerCurrentWidthDimension: CGFloat { if tabMode == .overflow { @@ -65,7 +65,7 @@ final class TabBarViewController: NSViewController { return HorizontalSpace.button.rawValue + HorizontalSpace.buttonPadding.rawValue } } - + required init?(coder: NSCoder) { fatalError("TabBarViewController: Bad initializer") } @@ -132,7 +132,7 @@ final class TabBarViewController: NSViewController { @objc func addButtonAction(_ sender: NSButton) { tabCollectionViewModel.appendNewTab(with: .homePage) } - + @IBAction func rightScrollButtonAction(_ sender: NSButton) { collectionView.scrollToEnd() } @@ -146,7 +146,7 @@ final class TabBarViewController: NSViewController { self?.reloadSelection() } } - + private func setupFireButton() { fireButton.toolTip = UserText.clearBrowsingHistoryTooltip fireButton.animationNames = MouseOverAnimationButton.AnimationNames(aqua: "flame-mouse-over", dark: "dark-flame-mouse-over") @@ -316,7 +316,7 @@ final class TabBarViewController: NSViewController { collectionView.selectItems(at: [newSelectionIndexPath], scrollPosition: .centeredHorizontally) } } - + private func selectTabWithPoint(_ point: NSPoint) { guard let pointLocationOnPinnedTabsView = pinnedTabsHostingView?.convert(point, from: view) else { return @@ -381,14 +381,14 @@ final class TabBarViewController: NSViewController { tabCollectionViewModel.remove(at: .unpinned(indexPath.item), published: false) WindowsManager.openNewWindow(with: tab, droppingPoint: droppingPoint) } - + // MARK: - Mouse Monitor - + private var mouseDownMonitor: Any? private func addMouseMonitors() { guard mouseDownMonitor == nil else { return } - + mouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in self?.mouseDown(with: event) } @@ -398,17 +398,17 @@ final class TabBarViewController: NSViewController { if let monitor = mouseDownMonitor { NSEvent.removeMonitor(monitor) } - + mouseDownMonitor = nil } - + func mouseDown(with event: NSEvent) -> NSEvent? { if event.window === view.window, view.window?.isMainWindow == false, let point = view.mouseLocationInsideBounds(event.locationInWindow) { selectTabWithPoint(point) } - + return event } @@ -534,7 +534,7 @@ final class TabBarViewController: NSViewController { leftShadowImageView.isHidden = scrollViewsAreHidden addTabButton.isHidden = scrollViewsAreHidden } - + private func setupAddTabButton() { addTabButton.target = self addTabButton.action = #selector(addButtonAction(_:)) @@ -987,7 +987,7 @@ extension TabBarViewController: NSCollectionViewDelegate { extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, isMouseOver: Bool) { - + if isMouseOver { // Show tab preview for visible tab bar items if collectionView.visibleRect.intersects(tabBarViewItem.view.frame) { diff --git a/DuckDuckGo/Tab Bar/View/TabBarViewItem.swift b/DuckDuckGo/Tab Bar/View/TabBarViewItem.swift index c0f8a20203..a0d376142b 100644 --- a/DuckDuckGo/Tab Bar/View/TabBarViewItem.swift +++ b/DuckDuckGo/Tab Bar/View/TabBarViewItem.swift @@ -270,7 +270,7 @@ final class TabBarViewItem: NSCollectionViewItem { layer.mask = layerMask return layer }() - + private lazy var layerMask: CALayer = { let layer = CALayer() layer.addSublayer(leftPixelMask) @@ -278,28 +278,28 @@ final class TabBarViewItem: NSCollectionViewItem { layer.addSublayer(topContentLineMask) return layer }() - + private lazy var leftPixelMask: CALayer = { let layer = CALayer() layer.backgroundColor = NSColor.white.cgColor return layer }() - + private lazy var rightPixelMask: CALayer = { let layer = CALayer() layer.backgroundColor = NSColor.white.cgColor return layer }() - + private lazy var topContentLineMask: CALayer = { let layer = CALayer() layer.backgroundColor = NSColor.white.cgColor return layer }() - + override func viewWillLayout() { super.viewWillLayout() - + withoutAnimation { borderLayer.frame = self.view.bounds leftPixelMask.frame = CGRect(x: 0, y: 0, width: TabShadowConfig.dividerSize, height: TabShadowConfig.dividerSize) @@ -307,7 +307,7 @@ final class TabBarViewItem: NSCollectionViewItem { topContentLineMask.frame = CGRect(x: 0, y: TabShadowConfig.dividerSize, width: borderLayer.bounds.width, height: borderLayer.bounds.height - TabShadowConfig.dividerSize) } } - + private func updateBorderLayerColor() { NSAppearance.withAppAppearance { withoutAnimation { @@ -315,7 +315,7 @@ final class TabBarViewItem: NSCollectionViewItem { } } } - + private func setupView() { mouseOverView.delegate = self mouseClickView.delegate = self @@ -326,7 +326,7 @@ final class TabBarViewItem: NSCollectionViewItem { view.layer?.masksToBounds = true view.layer?.addSublayer(borderLayer) } - + private func clearSubscriptions() { cancellables.removeAll() } @@ -346,9 +346,9 @@ final class TabBarViewItem: NSCollectionViewItem { faviconWrapperViewCenterConstraint.priority = titleTextField.isHidden ? .defaultHigh : .defaultLow faviconWrapperViewLeadingConstraint.priority = titleTextField.isHidden ? .defaultLow : .defaultHigh - + updateBorderLayerColor() - + if isSelected { borderLayer.isHidden = false } else { diff --git a/DuckDuckGo/Tab Bar/View/TabShadowView.swift b/DuckDuckGo/Tab Bar/View/TabShadowView.swift index fcc789fd30..d54db671ca 100644 --- a/DuckDuckGo/Tab Bar/View/TabShadowView.swift +++ b/DuckDuckGo/Tab Bar/View/TabShadowView.swift @@ -30,19 +30,19 @@ final class TabShadowView: NSView { super.init(coder: coder) setupSubviews() } - + override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } - + override func updateLayer() { super.updateLayer() } - + private func setupSubviews() { addSubview(shadowLine) } - + override func layout() { super.layout() shadowLine.wantsLayer = true diff --git a/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift index 3d2f8920ee..176afd9b3c 100644 --- a/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/Tab Bar/ViewModel/TabCollectionViewModel.swift @@ -520,12 +520,12 @@ final class TabCollectionViewModel: NSObject { insert(tab) } - + func title(forTabWithURL url: URL) -> String? { let matchingTab = tabCollection.tabs.first { tab in tab.url == url } - + return matchingTab?.title } diff --git a/DuckDuckGo/Tab Preview/View/TabPreviewWindowController.swift b/DuckDuckGo/Tab Preview/View/TabPreviewWindowController.swift index ceab506036..1f5b7cd473 100644 --- a/DuckDuckGo/Tab Preview/View/TabPreviewWindowController.swift +++ b/DuckDuckGo/Tab Preview/View/TabPreviewWindowController.swift @@ -44,7 +44,7 @@ final class TabPreviewWindowController: NSWindowController { override func windowDidLoad() { super.windowDidLoad() - + window?.animationBehavior = .utilityWindow NotificationCenter.default.addObserver(self, selector: #selector(suggestionWindowOpenNotification(_:)), diff --git a/DuckDuckGo/User Agent/Model/UserAgent.swift b/DuckDuckGo/User Agent/Model/UserAgent.swift index 77b609a817..8bdd7a8c2d 100644 --- a/DuckDuckGo/User Agent/Model/UserAgent.swift +++ b/DuckDuckGo/User Agent/Model/UserAgent.swift @@ -67,7 +67,7 @@ enum UserAgent { // use default WKWebView user agent for duckduckgo domain to remove CTA regex("https://duckduckgo\\.com/.*"): UserAgent.webViewDefault ] - + static func duckDuckGoUserAgent(appVersion: String = AppVersion.shared.versionNumber, appID: String = AppVersion.shared.identifier, systemVersion: String = ProcessInfo.processInfo.operatingSystemVersionString) -> String { diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 0ac6d083f3..0541968c78 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -50,16 +50,16 @@ final class WindowControllersManager: WindowControllersManagerProtocol { } } } - + private var mainWindowController: MainWindowController? { return mainWindowControllers.first(where: { let isMain = $0.window?.isMainWindow ?? false let hasMainChildWindow = $0.window?.childWindows?.contains { $0.isMainWindow } ?? false - + return $0.window?.isPopUpWindow == false && (isMain || hasMainChildWindow) }) } - + var selectedTab: Tab? { return mainWindowController?.mainViewController.tabCollectionViewModel.selectedTab } @@ -128,7 +128,7 @@ extension WindowControllersManager { show(url: bookmark.url) } } - + func show(url: URL?, newTab: Bool = false) { func show(url: URL?, in windowController: MainWindowController) { diff --git a/DuckDuckGo/Youtube Player/PrivatePlayerSchemeHandler.swift b/DuckDuckGo/Youtube Player/PrivatePlayerSchemeHandler.swift index 1c19d36ff3..05d89e671b 100644 --- a/DuckDuckGo/Youtube Player/PrivatePlayerSchemeHandler.swift +++ b/DuckDuckGo/Youtube Player/PrivatePlayerSchemeHandler.swift @@ -46,6 +46,6 @@ final class PrivatePlayerSchemeHandler: NSObject, WKURLSchemeHandler { urlSchemeTask.didFinish() } } - + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {} } diff --git a/DuckDuckGo/Youtube Player/YoutubeOverlayUserScript.swift b/DuckDuckGo/Youtube Player/YoutubeOverlayUserScript.swift index 20e6677d49..9382e01c87 100644 --- a/DuckDuckGo/Youtube Player/YoutubeOverlayUserScript.swift +++ b/DuckDuckGo/Youtube Player/YoutubeOverlayUserScript.swift @@ -203,7 +203,7 @@ extension YoutubeOverlayUserScript: WKScriptMessageHandlerWithReply { // MARK: - Fallback for macOS 10.15 extension YoutubeOverlayUserScript: WKScriptMessageHandler { - + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard isMessageFromVerifiedOrigin(message) else { return diff --git a/DuckDuckGo/Youtube Player/YoutubePlayerUserScript.swift b/DuckDuckGo/Youtube Player/YoutubePlayerUserScript.swift index d8935d1885..e5372d7528 100644 --- a/DuckDuckGo/Youtube Player/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/Youtube Player/YoutubePlayerUserScript.swift @@ -20,15 +20,15 @@ import WebKit import UserScript final class YoutubePlayerUserScript: NSObject, StaticUserScript { - + enum MessageNames: String, CaseIterable { case setAlwaysOpenSettingTo } - + public var requiresRunInPageContentWorld: Bool { return true } - + static var injectionTime: WKUserScriptInjectionTime { .atDocumentStart} static var forMainFrameOnly: Bool { true } static var source: String = "" @@ -56,22 +56,22 @@ final class YoutubePlayerUserScript: NSObject, StaticUserScript { assertionFailure("YoutubePlayerUserScript: unexpected message name \(message.name)") return } - + switch messageType { case .setAlwaysOpenSettingTo: handleAlwaysOpenSettings(message: message) } } - + private func handleAlwaysOpenSettings(message: WKScriptMessage) { guard let alwaysOpenOnPrivatePlayer = message.body as? Bool else { assertionFailure("YoutubePlayerUserScript: expected Bool") return } - + privatePlayerPreferences.privatePlayerMode = .init(alwaysOpenOnPrivatePlayer) } - + func evaluateJSCall(call: String, webView: WKWebView) { evaluate(js: call, inWebView: webView) } diff --git a/UI Tests/TabBarTests.swift b/UI Tests/TabBarTests.swift index fa74fd1c76..f59ef6991c 100644 --- a/UI Tests/TabBarTests.swift +++ b/UI Tests/TabBarTests.swift @@ -27,15 +27,15 @@ class TabBarTests: XCTestCase { func testWhenClickingAddTab_ThenTabsOpen() throws { let app = XCUIApplication() - + let tabbarviewitemElementsQuery = app.windows.collectionViews.otherElements.containing(.group, identifier: "TabBarViewItem") // click on add tab button twice tabbarviewitemElementsQuery.children(matching: .group).element(boundBy: 1).children(matching: .button).element.click() tabbarviewitemElementsQuery.children(matching: .group).element(boundBy: 2).children(matching: .button).element.click() - + let tabs = app.windows.collectionViews.otherElements.containing(.group, identifier: "TabBarViewItem").children(matching: .group) .matching(identifier: "TabBarViewItem") - + XCTAssertEqual(tabs.count, 3) } diff --git a/Unit Tests/App/WindowManagerStateRestorationTests.swift b/Unit Tests/App/WindowManagerStateRestorationTests.swift index 58eb5037d6..202c983001 100644 --- a/Unit Tests/App/WindowManagerStateRestorationTests.swift +++ b/Unit Tests/App/WindowManagerStateRestorationTests.swift @@ -53,7 +53,6 @@ final class WindowManagerStateRestorationTests: XCTestCase { // MARK: - - // swiftlint:disable:next function_body_length func testWindowManagerStateRestoration() throws { let tabs1 = [ Tab(content: .url(URL(string: "https://duckduckgo.com")!), diff --git a/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift b/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift index 0be0de4bc3..a6f4037cb2 100644 --- a/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift +++ b/Unit Tests/Autoconsent/AutoconsentMessageProtocolTests.swift @@ -37,12 +37,12 @@ class AutoconsentMessageProtocolTests: XCTestCase { tld: TLD()), config: MockPrivacyConfiguration() ) - + override func setUp() { super.setUp() PrivacySecurityPreferences.shared.autoconsentEnabled = true } - + func replyToJson(msg: Any) -> String { let jsonData = try? JSONSerialization.data(withJSONObject: msg, options: .sortedKeys) return String(data: jsonData!, encoding: .ascii)! @@ -66,7 +66,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { ) waitForExpectations(timeout: 1.0) } - + @MainActor func testInitResponds() { let expect = expectation(description: "tt") @@ -85,7 +85,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { XCTFail("Could not parse init response") return } - + XCTAssertEqual(dict["type"] as? String, "initResp") XCTAssertEqual(config["autoAction"] as? String, "optOut") }, @@ -93,7 +93,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { ) waitForExpectations(timeout: 1.0) } - + @MainActor func testEval() { let message = MockWKScriptMessage(name: "eval", body: [ @@ -113,7 +113,7 @@ class AutoconsentMessageProtocolTests: XCTestCase { ) waitForExpectations(timeout: 5.0) } - + @MainActor func testPopupFoundNoPromptIfEnabled() { let expect = expectation(description: "tt") @@ -137,15 +137,15 @@ class AutoconsentMessageProtocolTests: XCTestCase { @available(macOS 11, *) class MockWKScriptMessage: WKScriptMessage { - + let mockedName: String let mockedBody: Any let mockedWebView: WKWebView? - + override var name: String { return mockedName } - + override var body: Any { return mockedBody } @@ -153,7 +153,7 @@ class MockWKScriptMessage: WKScriptMessage { override var webView: WKWebView? { return mockedWebView } - + init(name: String, body: Any, webView: WKWebView? = nil) { self.mockedName = name self.mockedBody = body diff --git a/Unit Tests/Bitwarden/ConnectBitwardenViewModelTests.swift b/Unit Tests/Bitwarden/ConnectBitwardenViewModelTests.swift index 7fe670ab42..aca087b163 100644 --- a/Unit Tests/Bitwarden/ConnectBitwardenViewModelTests.swift +++ b/Unit Tests/Bitwarden/ConnectBitwardenViewModelTests.swift @@ -24,14 +24,14 @@ final class ConnectBitwardenViewModelTests: XCTestCase { func testWhenCreatingViewModel_ThenStatusIsDisclaimer() throws { let bitwardenManager = MockBitwardenManager() let viewModel = ConnectBitwardenViewModel(bitwardenManager: bitwardenManager) - + XCTAssertEqual(viewModel.viewState, .disclaimer) } - + func testWhenViewModelIsOnDisclaimer_AndBitwardenIsNotInstalled_AndNextIsClicked_ThenViewStateIsLookingForBitwarden() { let bitwardenManager = MockBitwardenManager() let viewModel = ConnectBitwardenViewModel(bitwardenManager: bitwardenManager) - + XCTAssertEqual(viewModel.viewState, .disclaimer) viewModel.process(action: .confirm) XCTAssertEqual(viewModel.viewState, .lookingForBitwarden) @@ -69,16 +69,16 @@ final class ConnectBitwardenViewModelTests: XCTestCase { XCTAssertTrue(bitwardenManager.handshakeSent) } - + func testWhenViewModelReceivesConnectStateFromManager_ThenViewStateIsConnectedToBitwarden() { let bitwardenManager = MockBitwardenManager() let viewModel = ConnectBitwardenViewModel(bitwardenManager: bitwardenManager) - + XCTAssertEqual(viewModel.viewState, .disclaimer) - + let vault = BWVault(id: "id", email: "dax@duck.com", status: .unlocked, active: true) bitwardenManager.status = .connected(vault: vault) - + XCTAssertEqual(viewModel.viewState, .connectedToBitwarden) } @@ -88,7 +88,7 @@ class MockBitwardenManager: BWManagement { var handshakeSent = false // var bitwardenStatus = BitwardenStatus.disabled - + @Published var status: BWStatus = .disabled var statusPublisher: Published.Publisher { $status } @@ -111,15 +111,15 @@ class MockBitwardenManager: BWManagement { func openBitwarden() { // no-op } - + func retrieveCredentials(for url: URL, completion: @escaping ([BWCredential], BWError?) -> Void) { // no-op } - + func create(credential: BWCredential, completion: @escaping (BWError?) -> Void) { // no-op } - + func update(credential: BWCredential, completion: @escaping (BWError?) -> Void) { // no-op } diff --git a/Unit Tests/Bookmarks Bar/ViewModel/BookmarksBarViewModelTests.swift b/Unit Tests/Bookmarks Bar/ViewModel/BookmarksBarViewModelTests.swift index 3cd6e6a3da..58eb00ba4c 100644 --- a/Unit Tests/Bookmarks Bar/ViewModel/BookmarksBarViewModelTests.swift +++ b/Unit Tests/Bookmarks Bar/ViewModel/BookmarksBarViewModelTests.swift @@ -20,17 +20,17 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser class BookmarksBarViewModelTests: XCTestCase { - + func testWhenClippingTheLastBarItem_AndNoItemsCanBeClipped_ThenNoItemsAreClipped() { let manager = createMockBookmarksManager() let bookmarksBarViewModel = BookmarksBarViewModel(bookmarkManager: manager, tabCollectionViewModel: .mock()) - + let clipped = bookmarksBarViewModel.clipLastBarItem() - + XCTAssertFalse(clipped) XCTAssert(bookmarksBarViewModel.clippedItems.isEmpty) } - + func testWhenClippingTheLastBarItem_AndItemsCanBeClipped_ThenItemsAreClipped() { let bookmarks = [Bookmark.mock] let storeMock = BookmarkStoreMock() @@ -39,13 +39,13 @@ class BookmarksBarViewModelTests: XCTestCase { let manager = createMockBookmarksManager(mockBookmarkStore: storeMock) let bookmarksBarViewModel = BookmarksBarViewModel(bookmarkManager: manager, tabCollectionViewModel: .mock()) bookmarksBarViewModel.update(from: bookmarks, containerWidth: 200) - + let clipped = bookmarksBarViewModel.clipLastBarItem() - + XCTAssertTrue(clipped) XCTAssertEqual(bookmarksBarViewModel.clippedItems.count, 1) } - + func testWhenTheBarHasClippedItems_ThenClippedItemsCanBeRestored() { let bookmarks = [Bookmark.mock] let storeMock = BookmarkStoreMock() @@ -54,18 +54,18 @@ class BookmarksBarViewModelTests: XCTestCase { let manager = createMockBookmarksManager(mockBookmarkStore: storeMock) let bookmarksBarViewModel = BookmarksBarViewModel(bookmarkManager: manager, tabCollectionViewModel: .mock()) bookmarksBarViewModel.update(from: bookmarks, containerWidth: 200) - + let clipped = bookmarksBarViewModel.clipLastBarItem() - + XCTAssert(clipped) XCTAssertEqual(bookmarksBarViewModel.clippedItems.count, 1) - + let restored = bookmarksBarViewModel.restoreLastClippedItem() - + XCTAssert(restored) XCTAssert(bookmarksBarViewModel.clippedItems.isEmpty) } - + func testWhenUpdatingFromBookmarkEntities_AndTheContainerCannotFitAnyBookmarks_ThenBookmarksAreImmediatelyClipped() { let bookmarks = [Bookmark.mock] let storeMock = BookmarkStoreMock() @@ -74,10 +74,10 @@ class BookmarksBarViewModelTests: XCTestCase { let manager = createMockBookmarksManager(mockBookmarkStore: storeMock) let bookmarksBarViewModel = BookmarksBarViewModel(bookmarkManager: manager, tabCollectionViewModel: .mock()) bookmarksBarViewModel.update(from: bookmarks, containerWidth: 0) - + XCTAssertEqual(bookmarksBarViewModel.clippedItems.count, 1) } - + func testWhenUpdatingFromBookmarkEntities_AndTheContainerCanFitAllBookmarks_ThenNoBookmarksAreClipped() { let bookmarks = [Bookmark.mock] let storeMock = BookmarkStoreMock() @@ -86,10 +86,10 @@ class BookmarksBarViewModelTests: XCTestCase { let manager = createMockBookmarksManager(mockBookmarkStore: storeMock) let bookmarksBarViewModel = BookmarksBarViewModel(bookmarkManager: manager, tabCollectionViewModel: .mock()) bookmarksBarViewModel.update(from: bookmarks, containerWidth: 200) - + XCTAssert(bookmarksBarViewModel.clippedItems.isEmpty) } - + private func createMockBookmarksManager(mockBookmarkStore: BookmarkStoreMock = BookmarkStoreMock()) -> BookmarkManager { let mockFaviconManager = FaviconManagerMock() return LocalBookmarkManager(bookmarkStore: mockBookmarkStore, faviconManagement: mockFaviconManager) diff --git a/Unit Tests/Bookmarks/Model/BookmarkManagedObjectTests.swift b/Unit Tests/Bookmarks/Model/BookmarkManagedObjectTests.swift index 32a8478fb3..eb3ef6b618 100644 --- a/Unit Tests/Bookmarks/Model/BookmarkManagedObjectTests.swift +++ b/Unit Tests/Bookmarks/Model/BookmarkManagedObjectTests.swift @@ -26,7 +26,7 @@ class BookmarkManagedObjectTests: XCTestCase { let container = CoreData.bookmarkContainer() let context = container.viewContext let parent = createTestRootFolderManagedObject(in: context) - + createTestBookmarkManagedObject(in: context, parent: parent) XCTAssertNoThrow(try context.save()) @@ -134,7 +134,7 @@ class BookmarkManagedObjectTests: XCTestCase { bottomLevelFolder.addToChildren(topLevelFolder) XCTAssertThrowsError(try context.save()) } - + func testWhenSavingBookmark_AndBookmarkDoesNotHaveParentFolder_ThenSavingFails() { let container = CoreData.bookmarkContainer() let context = container.viewContext @@ -152,7 +152,7 @@ class BookmarkManagedObjectTests: XCTestCase { XCTAssertEqual(error as? BookmarkManagedObject.BookmarkError, BookmarkManagedObject.BookmarkError.mustExistInsideRootFolder) } } - + func testWhenSavingFolder_AndFolderDoesNotHaveParentFolder_ThenSavingFails() { let container = CoreData.bookmarkContainer() let context = container.viewContext diff --git a/Unit Tests/Bookmarks/Services/BookmarkStoreMock.swift b/Unit Tests/Bookmarks/Services/BookmarkStoreMock.swift index d87182e7a5..7155c01d63 100644 --- a/Unit Tests/Bookmarks/Services/BookmarkStoreMock.swift +++ b/Unit Tests/Bookmarks/Services/BookmarkStoreMock.swift @@ -91,13 +91,13 @@ final class BookmarkStoreMock: BookmarkStore { importBookmarksCalled = true return BookmarkImportResult(successful: 0, duplicates: 0, failed: 0) } - + var canMoveObjectWithUUIDCalled = false func canMoveObjectWithUUID(objectUUID uuid: UUID, to parent: BookmarkFolder) -> Bool { canMoveObjectWithUUIDCalled = true return true } - + var moveObjectUUIDCalled = false func move(objectUUIDs: [UUID], toIndex: Int?, withinParentFolder: DuckDuckGo_Privacy_Browser.ParentFolderType, completion: @escaping (Error?) -> Void) { moveObjectUUIDCalled = true diff --git a/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift b/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift index dac13b7384..811480f368 100644 --- a/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift +++ b/Unit Tests/Bookmarks/Services/LocalBookmarkStoreTests.swift @@ -21,7 +21,6 @@ import Foundation import XCTest @testable import DuckDuckGo_Privacy_Browser -// swiftlint:disable:next type_body_length final class LocalBookmarkStoreTests: XCTestCase { // MARK: Save/Delete @@ -226,7 +225,7 @@ final class LocalBookmarkStoreTests: XCTestCase { waitForExpectations(timeout: 2, handler: nil) } - + func testWhenBookmarkIsAdded_AndFolderHasBeenProvided_ThenBookmarkIsSavedToParentFolder() { let container = CoreData.bookmarkContainer() let context = container.viewContext @@ -270,215 +269,215 @@ final class LocalBookmarkStoreTests: XCTestCase { waitForExpectations(timeout: 3, handler: nil) } - + // MARK: Moving Bookmarks/Folders - + func testWhenMovingBookmarkWithinParentCollection_AndIndexIsValid_ThenBookmarkIsMoved() async { let container = CoreData.bookmarkContainer() let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - + let folder = BookmarkFolder(id: UUID(), title: "Parent") let bookmark1 = Bookmark(id: UUID(), url: URL(string: "https://example1.com")!, title: "Example 1", isFavorite: false) let bookmark2 = Bookmark(id: UUID(), url: URL(string: "https://example2.com")!, title: "Example 2", isFavorite: false) let bookmark3 = Bookmark(id: UUID(), url: URL(string: "https://example3.com")!, title: "Example 3", isFavorite: false) - + // Save the initial bookmarks state: - + _ = await bookmarkStore.save(folder: folder, parent: nil) _ = await bookmarkStore.save(bookmark: bookmark1, parent: folder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark2, parent: folder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark3, parent: folder, index: nil) // Fetch persisted bookmarks back from the store: - + guard case let .success(initialTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let initialParentFolder = initialTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return } - + XCTAssertEqual(initialParentFolder.children.count, 3) - + // Verify initial order of saved bookmarks: - + let initialBookmarkUUIDs = [bookmark1.id, bookmark2.id, bookmark3.id] let initialFetchedBookmarkUUIDs = initialParentFolder.children.map(\.id) XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) - + // Update the order of the bookmarks: - + let moveBookmarksError = await bookmarkStore.move(objectUUIDs: [bookmark3.id], toIndex: 0, withinParentFolder: .parent(folder.id)) XCTAssertNil(moveBookmarksError) - + // Check the new bookmarks order: - + guard case let .success(updatedTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let updatedParentFolder = updatedTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return } - + let expectedBookmarkUUIDs = [bookmark3.id, bookmark1.id, bookmark2.id] let updatedFetchedBookmarkUUIDs = updatedParentFolder.children.map(\.id) XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } - + func testWhenMovingBookmarkWithinParentCollection_AndIndexIsOutOfBounds_ThenBookmarkIsAppended() async { let container = CoreData.bookmarkContainer() let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - + let initialParentFolder = BookmarkFolder(id: UUID(), title: "Parent") let bookmark1 = Bookmark(id: UUID(), url: URL(string: "https://example1.com")!, title: "Example 1", isFavorite: false) let bookmark2 = Bookmark(id: UUID(), url: URL(string: "https://example2.com")!, title: "Example 2", isFavorite: false) let bookmark3 = Bookmark(id: UUID(), url: URL(string: "https://example3.com")!, title: "Example 3", isFavorite: false) - + // Save the initial bookmarks state: - + _ = await bookmarkStore.save(folder: initialParentFolder, parent: nil) _ = await bookmarkStore.save(bookmark: bookmark1, parent: initialParentFolder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark2, parent: initialParentFolder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark3, parent: initialParentFolder, index: nil) // Fetch persisted bookmarks back from the store: - + guard case let .success(initialTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let initialParentFolder = initialTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return } - + XCTAssertEqual(initialParentFolder.children.count, 3) - + // Verify initial order of saved bookmarks: - + let initialBookmarkUUIDs = [bookmark1.id, bookmark2.id, bookmark3.id] let initialFetchedBookmarkUUIDs = initialParentFolder.children.map(\.id) XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) - + // Update the order of the bookmarks: - + let moveBookmarksError = await bookmarkStore.move(objectUUIDs: [bookmark1.id], toIndex: 999, withinParentFolder: .parent(initialParentFolder.id)) XCTAssertNil(moveBookmarksError) - + // Check the new bookmarks order: - + guard case let .success(updatedTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let updatedParentFolder = updatedTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return } - + let expectedBookmarkUUIDs = [bookmark2.id, bookmark3.id, bookmark1.id] let updatedFetchedBookmarkUUIDs = updatedParentFolder.children.map(\.id) XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } - + func testWhenMovingMultipleBookmarksWithinParentCollection_AndIndexIsValid_ThenBookmarksAreMoved() async { let container = CoreData.bookmarkContainer() let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - + let folder = BookmarkFolder(id: UUID(), title: "Parent") let bookmark1 = Bookmark(id: UUID(), url: URL(string: "https://example1.com")!, title: "Example 1", isFavorite: false) let bookmark2 = Bookmark(id: UUID(), url: URL(string: "https://example2.com")!, title: "Example 2", isFavorite: false) let bookmark3 = Bookmark(id: UUID(), url: URL(string: "https://example3.com")!, title: "Example 3", isFavorite: false) - + // Save the initial bookmarks state: - + _ = await bookmarkStore.save(folder: folder, parent: nil) _ = await bookmarkStore.save(bookmark: bookmark1, parent: folder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark2, parent: folder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark3, parent: folder, index: nil) // Fetch persisted bookmarks back from the store: - + guard case let .success(initialTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let initialParentFolder = initialTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return } - + XCTAssertEqual(initialParentFolder.children.count, 3) - + // Verify initial order of saved bookmarks: - + let initialBookmarkUUIDs = [bookmark1.id, bookmark2.id, bookmark3.id] let initialFetchedBookmarkUUIDs = initialParentFolder.children.map(\.id) XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) - + // Update the order of the bookmarks: - + let moveBookmarksError = await bookmarkStore.move(objectUUIDs: [bookmark1.id, bookmark2.id], toIndex: 3, withinParentFolder: .parent(folder.id)) XCTAssertNil(moveBookmarksError) - + // Check the new bookmarks order: - + guard case let .success(updatedTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let updatedParentFolder = updatedTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return } - + let expectedBookmarkUUIDs = [bookmark3.id, bookmark1.id, bookmark2.id] let updatedFetchedBookmarkUUIDs = updatedParentFolder.children.map(\.id) XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } - + func testWhenMovingBookmarkToRootFolder_AndIndexIsValid_ThenBookmarkIsMoved() async { guard let testState = await createInitialEntityMovementTestState() else { XCTFail("Failed to configure test state") return } - + // Update the order of the bookmarks: - + let moveBookmarksError = await testState.bookmarkStore.move(objectUUIDs: [testState.bookmark3.id], toIndex: 0, withinParentFolder: .root) XCTAssertNil(moveBookmarksError) - + // Check the new bookmarks order: - + guard case let .success(updatedTopLevelEntities) = await testState.bookmarkStore.loadAll(type: .topLevelEntities) else { XCTFail("Couldn't load top level entities") return } - + XCTAssertEqual(updatedTopLevelEntities.count, 2) - + let topLevelEntityIDs = updatedTopLevelEntities.map(\.id) XCTAssertEqual(topLevelEntityIDs, [testState.bookmark3.id, testState.initialParentFolder.id]) - + guard let folder = updatedTopLevelEntities.first(where: { $0.id == testState.initialParentFolder.id }) as? BookmarkFolder else { XCTFail("Couldn't find expected folder") return } - + let expectedBookmarkUUIDs = [testState.bookmark1.id, testState.bookmark2.id] let updatedFetchedBookmarkUUIDs = folder.children.map(\.id) XCTAssertEqual(expectedBookmarkUUIDs, updatedFetchedBookmarkUUIDs) } - + func testWhenMovingBookmarkToRootFolder_AndIndexIsOutOfBounds_ThenBookmarkIsAppended() async { guard let testState = await createInitialEntityMovementTestState() else { XCTFail("Failed to configure test state") return } - + // Update the order of the bookmarks: let moveBookmarksError = await testState.bookmarkStore.move(objectUUIDs: [testState.bookmark3.id], toIndex: 999, withinParentFolder: .root) XCTAssertNil(moveBookmarksError) - + // Check the new bookmarks order: - + guard case let .success(updatedTopLevelEntities) = await testState.bookmarkStore.loadAll(type: .topLevelEntities) else { XCTFail("Couldn't load top level entities") return } - + XCTAssertEqual(updatedTopLevelEntities.count, 2) - + let topLevelEntityIDs = updatedTopLevelEntities.map(\.id) XCTAssertEqual(topLevelEntityIDs, [testState.initialParentFolder.id, testState.bookmark3.id]) } @@ -722,47 +721,47 @@ final class LocalBookmarkStoreTests: XCTestCase { let bookmark3: Bookmark let initialParentFolder: BookmarkFolder } - + private func createInitialEntityMovementTestState() async -> EntityMovementTestState? { let container = CoreData.bookmarkContainer() let context = container.viewContext let bookmarkStore = LocalBookmarkStore(context: context) - + let initialParentFolder = BookmarkFolder(id: UUID(), title: "Parent") let bookmark1 = Bookmark(id: UUID(), url: URL(string: "https://example1.com")!, title: "Example 1", isFavorite: false) let bookmark2 = Bookmark(id: UUID(), url: URL(string: "https://example2.com")!, title: "Example 2", isFavorite: false) let bookmark3 = Bookmark(id: UUID(), url: URL(string: "https://example3.com")!, title: "Example 3", isFavorite: false) - + // Save the initial bookmarks state: - + _ = await bookmarkStore.save(folder: initialParentFolder, parent: nil) _ = await bookmarkStore.save(bookmark: bookmark1, parent: initialParentFolder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark2, parent: initialParentFolder, index: nil) _ = await bookmarkStore.save(bookmark: bookmark3, parent: initialParentFolder, index: nil) // Fetch persisted bookmarks back from the store: - + guard case let .success(initialTopLevelEntities) = await bookmarkStore.loadAll(type: .topLevelEntities), let initialParentFolder = initialTopLevelEntities.first as? BookmarkFolder else { XCTFail("Couldn't load top level entities") return nil } - + XCTAssertEqual(initialParentFolder.children.count, 3) - + // Verify initial order of saved bookmarks: - + let initialBookmarkUUIDs = [bookmark1.id, bookmark2.id, bookmark3.id] let initialFetchedBookmarkUUIDs = initialParentFolder.children.map(\.id) XCTAssertEqual(initialBookmarkUUIDs, initialFetchedBookmarkUUIDs) - + return EntityMovementTestState(bookmarkStore: bookmarkStore, bookmark1: bookmark1, bookmark2: bookmark2, bookmark3: bookmark3, initialParentFolder: initialParentFolder) } - + // MARK: Import func testWhenBookmarksAreImported_AndNoDuplicatesExist_ThenBookmarksAreImported() { @@ -811,7 +810,7 @@ final class LocalBookmarkStoreTests: XCTestCase { XCTAssertEqual(result.failed, 0) let loadResult = await bookmarkStore.loadAll(type: .bookmarks) - + switch loadResult { case .success(let bookmarks): XCTAssertEqual(bookmarks.count, 4) @@ -823,35 +822,35 @@ final class LocalBookmarkStoreTests: XCTestCase { func testWhenSafariBookmarksAreImported_AndTheBookmarksStoreIsEmpty_ThenBookmarksAreImportedToTheRootFolder_AndRootBookmarksAreFavorited() async { await validateInitialImport(for: .thirdPartyBrowser(.safari)) } - + func testWhenChromeBookmarksAreImported_AndTheBookmarksStoreIsEmpty_ThenBookmarksAreImportedToTheRootFolder_AndRootBookmarksAreFavorited() async { await validateInitialImport(for: .thirdPartyBrowser(.chrome)) } - + func testWhenFirefoxBookmarksAreImported_AndTheBookmarksStoreIsEmpty_ThenBookmarksAreImportedToTheRootFolder_AndRootBookmarksAreFavorited() async { await validateInitialImport(for: .thirdPartyBrowser(.firefox)) } - + func testWhenSafariBookmarksAreImported_AndTheBookmarksStoreIsNotEmpty_ThenBookmarksAreImportedToTheirOwnFolder_AndNoBookmarksAreFavorited() async { await validateSubsequentImport(for: .thirdPartyBrowser(.safari)) } - + func testWhenChromeBookmarksAreImported_AndTheBookmarksStoreIsNotEmpty_ThenBookmarksAreImportedToTheirOwnFolder_AndNoBookmarksAreFavorited() async { await validateSubsequentImport(for: .thirdPartyBrowser(.chrome)) } - + func testWhenFirefoxBookmarksAreImported_AndTheBookmarksStoreIsNotEmpty_ThenBookmarksAreImportedToTheirOwnFolder_AndNoBookmarksAreFavorited() async { await validateSubsequentImport(for: .thirdPartyBrowser(.firefox)) } - + func testWhenHTMLBookmarksAreImported_AndTheBookmarksStoreIsNotEmpty_ThenBookmarksAreImportedToTheirOwnFolder_AndNoBookmarksAreFavorited() async { await validateSubsequentImport(for: .thirdPartyBrowser(.bookmarksHTML)) } - + func testWhenDDGHTMLBookmarksAreImported_AndTheBookmarksStoreIsNotEmpty_ThenBookmarksAreImportedToTheirOwnFolder_AndNoBookmarksAreFavorited() async { await validateSubsequentImport(for: .duckduckgoWebKit) } - + private func validateInitialImport(for source: BookmarkImportSource) async { let container = CoreData.bookmarkContainer() let context = container.viewContext @@ -866,7 +865,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let topLevelEntitiesResult = await bookmarkStore.loadAll(type: .topLevelEntities) let bookmarksResult = await bookmarkStore.loadAll(type: .bookmarks) - + switch topLevelEntitiesResult { case .success(let entities): XCTAssert(entities.contains(where: { $0.title == "DuckDuckGo" })) @@ -874,7 +873,7 @@ final class LocalBookmarkStoreTests: XCTestCase { case .failure: XCTFail("Did not expect failure when checking topLevelEntitiesResult") } - + switch bookmarksResult { case .success(let bookmarks): var totalFavorites = 0 @@ -884,13 +883,13 @@ final class LocalBookmarkStoreTests: XCTestCase { totalFavorites += 1 } } - + XCTAssertEqual(totalFavorites, 1) case .failure: XCTFail("Did not expect failure when checking bookmarksResult") } } - + private func validateSubsequentImport(for source: BookmarkImportSource) async { let container = CoreData.bookmarkContainer() let context = container.viewContext @@ -903,7 +902,7 @@ final class LocalBookmarkStoreTests: XCTestCase { let topLevelEntitiesResult = await bookmarkStore.loadAll(type: .topLevelEntities) let bookmarksResult = await bookmarkStore.loadAll(type: .bookmarks) - + switch topLevelEntitiesResult { case .success(let entities): XCTAssert(entities.contains(where: { $0.title == "DuckDuckGo" })) @@ -912,7 +911,7 @@ final class LocalBookmarkStoreTests: XCTestCase { case .failure: XCTFail("Did not expect failure when checking topLevelEntitiesResult") } - + switch bookmarksResult { case .success(let bookmarks): var totalFavorites = 0 @@ -922,13 +921,13 @@ final class LocalBookmarkStoreTests: XCTestCase { totalFavorites += 1 } } - + XCTAssertEqual(totalFavorites, 1) case .failure: XCTFail("Did not expect failure when checking bookmarksResult") } } - + private func createMockImportedBookmarks() -> ImportedBookmarks { let bookmark1 = ImportedBookmarks.BookmarkOrFolder(name: "DuckDuckGo", type: "bookmark", urlString: "https://duckduckgo.com", children: nil) let bookmark2 = ImportedBookmarks.BookmarkOrFolder(name: "Duck", type: "bookmark", urlString: "https://duck.com", children: nil) @@ -938,8 +937,8 @@ final class LocalBookmarkStoreTests: XCTestCase { let otherBookmarks = ImportedBookmarks.BookmarkOrFolder(name: "Other Bookmarks", type: "folder", urlString: nil, children: []) let topLevelFolders = ImportedBookmarks.TopLevelFolders(bookmarkBar: bookmarkBar, otherBookmarks: otherBookmarks) - + return ImportedBookmarks(topLevelFolders: topLevelFolders) } - + } diff --git a/Unit Tests/Browser Tab/Model/TabTests.swift b/Unit Tests/Browser Tab/Model/TabTests.swift index cb6d66f008..c43ad36611 100644 --- a/Unit Tests/Browser Tab/Model/TabTests.swift +++ b/Unit Tests/Browser Tab/Model/TabTests.swift @@ -46,7 +46,7 @@ final class TabTests: XCTestCase { XCTAssert(tab != tab2) } - + } extension Tab { diff --git a/Unit Tests/Browser Tab/Services/FaviconManagerMock.swift b/Unit Tests/Browser Tab/Services/FaviconManagerMock.swift index 2417cf2c42..9121e5eb1c 100644 --- a/Unit Tests/Browser Tab/Services/FaviconManagerMock.swift +++ b/Unit Tests/Browser Tab/Services/FaviconManagerMock.swift @@ -29,7 +29,7 @@ final class FaviconManagerMock: FaviconManagement { func handleFaviconLinks(_ faviconLinks: [FaviconUserScript.FaviconLink], documentUrl: URL, completion: @escaping (Favicon?) -> Void) { completion(nil) } - + func handleFavicons(_ favicons: [Favicon], documentUrl: URL) { // no-op } diff --git a/Unit Tests/Browser Tab/Services/WebsiteDataStoreTests.swift b/Unit Tests/Browser Tab/Services/WebsiteDataStoreTests.swift index fc797bd8e2..0465802f39 100644 --- a/Unit Tests/Browser Tab/Services/WebsiteDataStoreTests.swift +++ b/Unit Tests/Browser Tab/Services/WebsiteDataStoreTests.swift @@ -181,7 +181,7 @@ final class WebCacheManagerTests: XCTestCase { } } } - + func removeData(ofTypes dataTypes: Set, for records: [WKWebsiteDataRecord]) async { removeDataCalledCount += 1 diff --git a/Unit Tests/Common/CoreDataTestUtilities.swift b/Unit Tests/Common/CoreDataTestUtilities.swift index d66c7b972a..8326a174af 100644 --- a/Unit Tests/Common/CoreDataTestUtilities.swift +++ b/Unit Tests/Common/CoreDataTestUtilities.swift @@ -21,7 +21,7 @@ import CoreData @testable import DuckDuckGo_Privacy_Browser final class CoreData { - + static func historyStoreContainer() -> NSPersistentContainer { createInMemoryPersistentContainer(modelName: "History", bundle: Bundle(for: AppDelegate.self)) } diff --git a/Unit Tests/Common/Database/CoreDataStoreTests.swift b/Unit Tests/Common/Database/CoreDataStoreTests.swift index a6ecb4d4a9..cf8a161b64 100644 --- a/Unit Tests/Common/Database/CoreDataStoreTests.swift +++ b/Unit Tests/Common/Database/CoreDataStoreTests.swift @@ -133,7 +133,7 @@ final class CoreDataStoreTests: XCTestCase { store.clear { [store] error in XCTAssertNil(error) - let fireproofed = try! store.load(into: .init(), self.load) // swiftlint:disable:this force_try + let fireproofed = try! store.load(into: .init(), self.load) XCTAssertEqual(fireproofed, [:]) diff --git a/Unit Tests/Common/Extensions/RunLoopExtensionTests.swift b/Unit Tests/Common/Extensions/RunLoopExtensionTests.swift index 370c72befc..b356f55158 100644 --- a/Unit Tests/Common/Extensions/RunLoopExtensionTests.swift +++ b/Unit Tests/Common/Extensions/RunLoopExtensionTests.swift @@ -38,7 +38,7 @@ final class RunLoopExtensionTests: XCTestCase { waitForExpectations(timeout: 1) } - + func testWhenConditionIsResolvedThenWaitIsFinished() { let condition = RunLoop.ResumeCondition() diff --git a/Unit Tests/Common/Extensions/WKWebsiteDataStoreExtensionTests.swift b/Unit Tests/Common/Extensions/WKWebsiteDataStoreExtensionTests.swift index cc6789d5ad..b9c3ca2b97 100644 --- a/Unit Tests/Common/Extensions/WKWebsiteDataStoreExtensionTests.swift +++ b/Unit Tests/Common/Extensions/WKWebsiteDataStoreExtensionTests.swift @@ -21,26 +21,26 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser final class WKWebsiteDataStoreExtensionTests: XCTestCase { - + func testWhenGettingRemovableDataTypes_ThenLocalStorageAndIndexedDBAreNotIncluded() { let removableTypes = WKWebsiteDataStore.safelyRemovableWebsiteDataTypes - + XCTAssertFalse(removableTypes.contains(WKWebsiteDataTypeLocalStorage)) - + if #available(macOS 12.2, *) { XCTAssertFalse(removableTypes.contains(WKWebsiteDataTypeIndexedDBDatabases)) } else { XCTAssertTrue(removableTypes.contains(WKWebsiteDataTypeIndexedDBDatabases)) } } - + func testWhenGettingAllWebsiteDataTypesExceptCookies_ThenLocalStorageAndIndexedDBAreIncluded() { let removableTypes = WKWebsiteDataStore.allWebsiteDataTypesExceptCookies - + XCTAssertFalse(removableTypes.contains(WKWebsiteDataTypeCookies)) - + XCTAssertTrue(removableTypes.contains(WKWebsiteDataTypeLocalStorage)) XCTAssertTrue(removableTypes.contains(WKWebsiteDataTypeIndexedDBDatabases)) } - + } diff --git a/Unit Tests/Common/File System/FileStoreMock.swift b/Unit Tests/Common/File System/FileStoreMock.swift index cce2ab9bba..7f22b5fa7c 100644 --- a/Unit Tests/Common/File System/FileStoreMock.swift +++ b/Unit Tests/Common/File System/FileStoreMock.swift @@ -24,14 +24,12 @@ final class FileStoreMock: NSObject, FileStore { private var _fileStorage = [String: Data]() private let fileStorageLock = NSLock() @objc dynamic var storage: [String: Data] { - // swiftlint:disable implicit_getter get { fileStorageLock.lock() defer { fileStorageLock.unlock() } return _fileStorage } - // swiftlint:enable implicit_getter _modify { fileStorageLock.lock() defer { @@ -41,18 +39,16 @@ final class FileStoreMock: NSObject, FileStore { yield &_fileStorage } } - + private var _directoryStorage = [String: [String]]() private let directoryStorageLock = NSLock() @objc dynamic var directoryStorage: [String: [String]] { - // swiftlint:disable implicit_getter get { directoryStorageLock.lock() defer { directoryStorageLock.unlock() } return _directoryStorage } - // swiftlint:enable implicit_getter _modify { directoryStorageLock.lock() defer { @@ -93,7 +89,7 @@ final class FileStoreMock: NSObject, FileStore { func directoryContents(at path: String) throws -> [String] { return directoryStorage[path] ?? [] } - + func remove(fileAtURL url: URL) { storage[url.lastPathComponent] = nil } diff --git a/Unit Tests/Common/File System/TemporaryFileHandlerTests.swift b/Unit Tests/Common/File System/TemporaryFileHandlerTests.swift index ede9b47a41..a7fd72dd1a 100644 --- a/Unit Tests/Common/File System/TemporaryFileHandlerTests.swift +++ b/Unit Tests/Common/File System/TemporaryFileHandlerTests.swift @@ -21,28 +21,28 @@ import Combine @testable import DuckDuckGo_Privacy_Browser final class TemporaryFileHandlerTests: XCTestCase { - + func testWhenPassingAValidPathToTheTemporaryFileHandler_ThenTheFileIsCopied_AndDeletedAfterTheHandlerIsComplete() throws { let handler = TemporaryFileHandler(fileURL: loginDatabaseURL()) var temporaryFileURL: URL? - + try handler.withTemporaryFile { url in temporaryFileURL = url XCTAssertTrue(url.path.contains(NSTemporaryDirectory())) XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) XCTAssertEqual(url.pathExtension, "db") } - + if let temporaryFileURL = temporaryFileURL { XCTAssertFalse(FileManager.default.fileExists(atPath: temporaryFileURL.path)) } else { XCTFail("Did not get temporary file URL") } } - + private func loginDatabaseURL() -> URL { let bundle = Bundle(for: TemporaryFileHandlerTests.self) return bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data/No Primary Password/key4.db") } - + } diff --git a/Unit Tests/Common/FileSystemDSL.swift b/Unit Tests/Common/FileSystemDSL.swift index b16662d6d7..04d62d5cbc 100644 --- a/Unit Tests/Common/FileSystemDSL.swift +++ b/Unit Tests/Common/FileSystemDSL.swift @@ -22,7 +22,7 @@ enum FileSystemEntity { case file(name: String, contents: File.FileContents) case directory(name: String, children: [FileSystemEntity]) - + var name: String { switch self { case .file(let name, _): return name @@ -35,19 +35,19 @@ enum FileSystemEntity { protocol FileSystemEntityConvertible { func asFileSystemEntity() -> FileSystemEntity - + } struct Directory: FileSystemEntityConvertible { let name: String let children: [FileSystemEntity] - + init(_ name: String, @FileDirectoryStructureBuilder builder: () -> [FileSystemEntity]) { self.name = name self.children = builder() } - + func asFileSystemEntity() -> FileSystemEntity { return .directory(name: name, children: children) } @@ -60,15 +60,15 @@ struct File: FileSystemEntityConvertible { case string(String) case copy(URL) } - + let name: String let contents: FileContents - + init(_ name: String, contents: FileContents) { self.name = name self.contents = contents } - + func asFileSystemEntity() -> FileSystemEntity { return .file(name: name, contents: contents) } @@ -81,14 +81,14 @@ struct FileDirectoryStructureBuilder { static func buildBlock(_ elements: FileSystemEntityConvertible...) -> [FileSystemEntity] { return elements.compactMap { $0.asFileSystemEntity() } } - + } struct FileSystem { var rootDirectoryName: String var children: [FileSystemEntity] - + var rootDirectoryURL: URL { let temporaryURL = FileManager.default.temporaryDirectory return temporaryURL.appendingPathComponent(rootDirectoryName) @@ -98,22 +98,22 @@ struct FileSystem { self.rootDirectoryName = rootDirectoryName self.children = builder() } - + func writeToTemporaryDirectory() throws { try FileManager.default.createDirectory(at: rootDirectoryURL, withIntermediateDirectories: false) - + for entity in children { try persist(entity: entity, parentDirectoryURL: rootDirectoryURL) } } - + func removeCreatedFileSystemStructure() throws { try FileManager.default.removeItem(at: rootDirectoryURL) } - + private func persist(entity: FileSystemEntity, parentDirectoryURL: URL) throws { let entityURL = parentDirectoryURL.appendingPathComponent(entity.name) - + switch entity { case .file(_, let contents): switch contents { @@ -124,7 +124,7 @@ struct FileSystem { } case .directory(_, let children): try FileManager.default.createDirectory(at: entityURL, withIntermediateDirectories: false) - + for directoryChild in children { try persist(entity: directoryChild, parentDirectoryURL: entityURL) } diff --git a/Unit Tests/Common/FileSystemDSLTests.swift b/Unit Tests/Common/FileSystemDSLTests.swift index 3324966a8b..501c0d3db9 100644 --- a/Unit Tests/Common/FileSystemDSLTests.swift +++ b/Unit Tests/Common/FileSystemDSLTests.swift @@ -23,13 +23,13 @@ import XCTest final class FileSystemDSLTests: XCTestCase { private let rootDirectoryName = UUID().uuidString - + override func setUp() { super.setUp() let defaultRootDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) try? FileManager.default.removeItem(at: defaultRootDirectoryURL) } - + override func tearDown() { super.tearDown() let defaultRootDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) @@ -38,104 +38,104 @@ final class FileSystemDSLTests: XCTestCase { func testWhenWritingFileSystemStructure_ThenRootDirectoryIsCreated() throws { let structure = FileSystem(rootDirectoryName: rootDirectoryName) { } - + let expectedURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) XCTAssertFalse(FileManager.default.fileExists(atPath: expectedURL.path)) - + try structure.writeToTemporaryDirectory() XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL.path)) - + try structure.removeCreatedFileSystemStructure() XCTAssertFalse(FileManager.default.fileExists(atPath: expectedURL.path)) } - + func testWhenWritingNestedFileSystemStructure_ThenStructureIsCreated() throws { let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("top-level-file", contents: .string("")) - + Directory("folder-1") { File("nested-file", contents: .string("")) - + Directory("nested-folder") { File("even-deeper-file", contents: .string("")) } } - + Directory("folder-2") { File("second-nested-file", contents: .string("")) File("third-nested-file", contents: .string("")) } } - + let expectedURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) XCTAssertFalse(FileManager.default.fileExists(atPath: expectedURL.path)) - + try structure.writeToTemporaryDirectory() XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL.appendingPathComponent("top-level-file").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL.appendingPathComponent("folder-1").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL .appendingPathComponent("folder-1") .appendingPathComponent("nested-file").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL .appendingPathComponent("folder-1") .appendingPathComponent("nested-folder").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL .appendingPathComponent("folder-1") .appendingPathComponent("nested-folder") .appendingPathComponent("even-deeper-file").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL.appendingPathComponent("folder-2").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL .appendingPathComponent("folder-2") .appendingPathComponent("second-nested-file").path)) - + XCTAssertTrue(FileManager.default.fileExists(atPath: expectedURL .appendingPathComponent("folder-2") .appendingPathComponent("third-nested-file").path)) - + try structure.removeCreatedFileSystemStructure() XCTAssertFalse(FileManager.default.fileExists(atPath: expectedURL.path)) } - + func testWhenPersistingFile_AndFileContentsAreAString_ThenTheFileContentsAreCorrect() throws { let expectedContents = "Top Level File Contents" - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("top-level-file", contents: .string(expectedContents)) } - + try structure.writeToTemporaryDirectory() - + let filePath = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName).appendingPathComponent("top-level-file") let contents = try String(contentsOf: filePath) - + XCTAssertEqual(contents, expectedContents) - + try structure.removeCreatedFileSystemStructure() } - + func testWhenPersistingFile_AndFileContentsAreCopied_ThenTheFileContentsAreCorrect() throws { let bundle = Bundle(for: FileSystemDSLTests.self) let bundleFileURL = bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data/No Primary Password/key4.db") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(bundleFileURL)) } - + try structure.writeToTemporaryDirectory() - + let copiedFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName).appendingPathComponent("key4.db") let bundleFileContents = try Data(contentsOf: bundleFileURL) let copiedFileContents = try Data(contentsOf: copiedFileURL) - + XCTAssertEqual(bundleFileContents, copiedFileContents) - + try structure.removeCreatedFileSystemStructure() } diff --git a/Unit Tests/Configuration/ConfigurationDownloaderTests.swift b/Unit Tests/Configuration/ConfigurationDownloaderTests.swift index 34d59d251e..b42b2c13e5 100644 --- a/Unit Tests/Configuration/ConfigurationDownloaderTests.swift +++ b/Unit Tests/Configuration/ConfigurationDownloaderTests.swift @@ -25,7 +25,7 @@ final class ConfigurationDownloaderTests: XCTestCase { static let resultData = "test".data(using: .utf8)! var cancellables = Set() - + func test_urls_do_not_contain_localhost() { for url in ConfigurationLocation.allCases { XCTAssertFalse(url.rawValue.contains("localhost")) diff --git a/Unit Tests/Content Blocker/AppPrivacyConfigurationTests.swift b/Unit Tests/Content Blocker/AppPrivacyConfigurationTests.swift index e952b460d0..673fcc19c1 100644 --- a/Unit Tests/Content Blocker/AppPrivacyConfigurationTests.swift +++ b/Unit Tests/Content Blocker/AppPrivacyConfigurationTests.swift @@ -43,11 +43,11 @@ class AppPrivacyConfigurationTests: XCTestCase { AppPrivacyConfigurationDataProvider.Constants.embeddedDataSHA, "Error: please update SHA and ETag when changing embedded config") } - + func testWhenEmbeddedDataIsUsedThenItCanBeParsed() { - + let provider = AppPrivacyConfigurationDataProvider() - + let jsonData = provider.embeddedData let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] let configData = PrivacyConfigurationData(json: json!) @@ -55,9 +55,9 @@ class AppPrivacyConfigurationTests: XCTestCase { let config = AppPrivacyConfiguration(data: configData, identifier: "", localProtection: MockDomainsProtectionStore()) - + XCTAssert(config.isEnabled(featureKey: .contentBlocking)) - + } } diff --git a/Unit Tests/Content Blocker/ClickToLoadTDSTests.swift b/Unit Tests/Content Blocker/ClickToLoadTDSTests.swift index 127d946ab9..77f6d6f2f9 100644 --- a/Unit Tests/Content Blocker/ClickToLoadTDSTests.swift +++ b/Unit Tests/Content Blocker/ClickToLoadTDSTests.swift @@ -22,9 +22,9 @@ import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser class ClickToLoadTDSTests: XCTestCase { - + func testEnsureClickToLoadTDSCompiles() throws { - + let tds = ContentBlockerRulesLists.fbTrackerDataSet let builder = ContentBlockerRulesBuilder(trackerData: tds) @@ -38,22 +38,22 @@ class ClickToLoadTDSTests: XCTestCase { let identifier = UUID().uuidString let compiled = expectation(description: "Rules compiled") - + WKContentRuleListStore.default().compileContentRuleList(forIdentifier: identifier, encodedContentRuleList: ruleList) { result, error in XCTAssertNotNil(result) XCTAssertNil(error) compiled.fulfill() } - + wait(for: [compiled], timeout: 30.0) - + let removed = expectation(description: "Rules removed") - + WKContentRuleListStore.default().removeContentRuleList(forIdentifier: identifier) { _ in removed.fulfill() } - + wait(for: [removed], timeout: 5.0) } } diff --git a/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift b/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift index 57c9cdceab..4b9446a30b 100644 --- a/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift +++ b/Unit Tests/Content Blocker/ContentBlockingUpdatingTests.swift @@ -177,11 +177,11 @@ class ContentBlockingUpdatingTests: XCTestCase { } extension UserContentControllerNewContent { - + func rules(withName name: String) -> WKContentRuleList? { rulesUpdate.rules.first(where: { $0.name == name})?.rulesList } - + var isValid: Bool { return rules(withName: "test") != nil } diff --git a/Unit Tests/Content Blocker/EmbeddedTrackerDataTests.swift b/Unit Tests/Content Blocker/EmbeddedTrackerDataTests.swift index 83622c1e55..e352bbe2ae 100644 --- a/Unit Tests/Content Blocker/EmbeddedTrackerDataTests.swift +++ b/Unit Tests/Content Blocker/EmbeddedTrackerDataTests.swift @@ -23,53 +23,53 @@ import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser class EmbeddedTrackerDataTests: XCTestCase { - + func testWhenEmbeddedDataIsUpdatedThenUpdateSHAAndEtag() throws { - + let hash = try Data(contentsOf: AppTrackerDataSetProvider.embeddedUrl).sha256 XCTAssertEqual(hash, AppTrackerDataSetProvider.Constants.embeddedDataSHA, "Error: please update SHA and ETag when changing embedded TDS") } - + func testWhenEmbeddedDataIsPresentThenWeCanUseItToLookupTrackers() { let manager = TrackerDataManager(etag: nil, data: nil, embeddedDataProvider: AppTrackerDataSetProvider()) - + XCTAssertEqual(manager.trackerData.findEntity(forHost: "www.google.com")?.displayName, "Google") } - + func testWhenEmbeddedDataIsCompiledThenThereIsNoError() throws { - + let embeddedData = try Data(contentsOf: AppTrackerDataSetProvider.embeddedUrl) let tds = try JSONDecoder().decode(TrackerData.self, from: embeddedData) let builder = ContentBlockerRulesBuilder(trackerData: tds) - + let rules = builder.buildRules(withExceptions: [], andTemporaryUnprotectedDomains: [], andTrackerAllowlist: []) - + let data = try JSONEncoder().encode(rules) let ruleList = String(data: data, encoding: .utf8)! - + let identifier = UUID().uuidString - + let compiled = expectation(description: "Rules compiled") - + WKContentRuleListStore.default().compileContentRuleList(forIdentifier: identifier, encodedContentRuleList: ruleList) { result, error in XCTAssertNotNil(result) XCTAssertNil(error) compiled.fulfill() } - + wait(for: [compiled], timeout: 30.0) - + let removed = expectation(description: "Rules removed") - + WKContentRuleListStore.default().removeContentRuleList(forIdentifier: identifier) { _ in removed.fulfill() } - + wait(for: [removed], timeout: 5.0) } } diff --git a/Unit Tests/Crash Reports/CrashReportTests.swift b/Unit Tests/Crash Reports/CrashReportTests.swift index f4ccd4d16b..b67cfc5496 100644 --- a/Unit Tests/Crash Reports/CrashReportTests.swift +++ b/Unit Tests/Crash Reports/CrashReportTests.swift @@ -24,21 +24,21 @@ class CrashReportTests: XCTestCase { func testWhenParsingIPSCrashReports_ThenCrashReportDataDoesNotIncludeIdentifyingInformation() { let bundle = Bundle(for: CrashReportTests.self) let url = bundle.resourceURL!.appendingPathComponent("DuckDuckGo-ExampleCrash.ips") - + let report = JSONCrashReport(url: url) - + XCTAssertNotNil(report.content) XCTAssertNotNil(report.contentData) - + // Verify that the content includes the slice ID XCTAssertTrue(report.content!.contains("7fc6ff2c-a85d-3116-96d9-9368ec955ba1")) - + // Verify that the content does not include the sleepWakeUUID anywhere in the report XCTAssertFalse(report.content!.contains("2384290E-F858-4024-9488-11D3FF94B4DD")) - + // Verify that the content does not include the device identitier XCTAssertFalse(report.content!.contains("483B097A-A969-596F-9F2A-357347BB1DEC")) - + // Verify that the content does not include any experiment rollout identifiers XCTAssertFalse(report.content!.contains("602ad4dac86151000cf27e46")) XCTAssertFalse(report.content!.contains("5fc94383418129005b4e9ae0")) @@ -46,12 +46,12 @@ class CrashReportTests: XCTestCase { XCTAssertFalse(report.content!.contains("60da5e84ab0ca017dace9abf")) XCTAssertFalse(report.content!.contains("607844aa04477260f58a8077")) XCTAssertFalse(report.content!.contains("601d9415f79519000ccd4b69")) - + // Verify that the sleepWakeUUID is definitely empty XCTAssert(report.content!.contains(#"sleepWakeUUID":""#)) XCTAssert(report.content!.contains(#"deviceIdentifierForVendor":""#)) } - + private func ipsCrashURL() -> URL { let bundle = Bundle(for: CrashReportTests.self) return bundle.resourceURL!.appendingPathComponent("DuckDuckGo-ExampleCrash.ips") diff --git a/Unit Tests/Data Export/MockSecureVault.swift b/Unit Tests/Data Export/MockSecureVault.swift index ddee969351..84a38a544e 100644 --- a/Unit Tests/Data Export/MockSecureVault.swift +++ b/Unit Tests/Data Export/MockSecureVault.swift @@ -106,11 +106,11 @@ final class MockSecureVault: SecureVault { func deleteCreditCardFor(cardId: Int64) throws { storedCards = storedCards.filter { $0.id != cardId } } - + func existingIdentityForAutofill(matching proposedIdentity: SecureVaultModels.Identity) throws -> SecureVaultModels.Identity? { return nil } - + func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard? { return nil } diff --git a/Unit Tests/Data Import/BrowserProfileTests.swift b/Unit Tests/Data Import/BrowserProfileTests.swift index 3a8e918af1..1a8c1357f1 100644 --- a/Unit Tests/Data Import/BrowserProfileTests.swift +++ b/Unit Tests/Data Import/BrowserProfileTests.swift @@ -21,90 +21,90 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser class BrowserProfileListTests: XCTestCase { - + let mockURL = URL(string: "/Users/Dax/Library/ApplicationSupport/BrowserCompany/Browser/")! func testWhenBrowserProfileHasURLWithNoLoginData_ThenHasLoginDataIsFalse() { let profileURL = profile(named: "Profile") let fileStore = FileStoreMock() let profile = DataImport.BrowserProfile(profileURL: profileURL, fileStore: fileStore) - + XCTAssertFalse(profile.hasLoginData) } - + func testWhenBrowserProfileHasURLWithChromiumLoginData_ThenHasLoginDataIsTrue() { let profileURL = profile(named: "Profile") let fileStore = FileStoreMock() let profile = DataImport.BrowserProfile(profileURL: profileURL, fileStore: fileStore) - + fileStore.directoryStorage[profileURL.absoluteString] = ["Login Data"] - + XCTAssertTrue(profile.hasLoginData) } - + func testWhenBrowserProfileHasURLWithFirefoxLoginData_ThenHasLoginDataIsTrue() { let profileURL = profile(named: "Profile") let fileStore = FileStoreMock() let profile = DataImport.BrowserProfile(profileURL: profileURL, fileStore: fileStore) - + fileStore.directoryStorage[profileURL.absoluteString] = ["key4.db"] XCTAssertFalse(profile.hasLoginData) - + fileStore.directoryStorage[profileURL.absoluteString] = ["logins.json"] XCTAssertFalse(profile.hasLoginData) - + fileStore.directoryStorage[profileURL.absoluteString] = ["logins.json", "key4.db"] XCTAssertTrue(profile.hasLoginData) } - + func testWhenGettingProfileName_AndProfileHasNoDetectedName_ThenTheDirectoryNameIsUsed() { let profileURL = profile(named: "Profile") let fileStore = FileStoreMock() let profile = DataImport.BrowserProfile(profileURL: profileURL, fileStore: fileStore) - + XCTAssertEqual(profile.profileName, "Profile") } - + func testWhenGettingProfileName_AndProfileHasNoDetectedChromiumName_ThenDetectedNameIsUsed() { let profileURL = profile(named: "DirectoryName") let fileStore = FileStoreMock() - + let chromiumPreferences = ChromePreferences(profile: .init(name: "ChromeProfile")) guard let chromiumPreferencesData = try? JSONEncoder().encode(chromiumPreferences) else { XCTFail("Failed to encode Chromium preferences object") return } - + fileStore.storage["Preferences"] = chromiumPreferencesData fileStore.directoryStorage[profileURL.absoluteString] = ["Preferences"] - + let profile = DataImport.BrowserProfile(profileURL: profileURL, fileStore: fileStore) - + XCTAssertEqual(profile.profileName, "ChromeProfile") XCTAssertTrue(profile.hasNonDefaultProfileName) } - + func testWhenGettingProfileName_AndChromiumPreferencesAreDetected_AndProfileNameIsSystemProfile_ThenProfileHasDefaultProfileName() { let profileURL = profile(named: "System Profile") let fileStore = FileStoreMock() - + let chromiumPreferences = ChromePreferences(profile: .init(name: "ChromeProfile")) guard let chromiumPreferencesData = try? JSONEncoder().encode(chromiumPreferences) else { XCTFail("Failed to encode Chromium preferences object") return } - + fileStore.storage["Preferences"] = chromiumPreferencesData fileStore.directoryStorage[profileURL.absoluteString] = ["Preferences"] - + let profile = DataImport.BrowserProfile(profileURL: profileURL, fileStore: fileStore) - + XCTAssertEqual(profile.profileName, "System Profile") XCTAssertFalse(profile.hasNonDefaultProfileName) } - + private func profile(named name: String) -> URL { return mockURL.appendingPathComponent(name) } - + } diff --git a/Unit Tests/Data Import/ChromiumLoginReaderTests.swift b/Unit Tests/Data Import/ChromiumLoginReaderTests.swift index d9551d0085..bb10bf747b 100644 --- a/Unit Tests/Data Import/ChromiumLoginReaderTests.swift +++ b/Unit Tests/Data Import/ChromiumLoginReaderTests.swift @@ -82,7 +82,7 @@ class ChromiumLoginReaderTests: XCTestCase { XCTAssertEqual(logins[0].username, "username") XCTAssertEqual(logins[0].password, "password") } - + func testWhenImportingChromiumData_AndTheUserCancelsTheKeychainPrompt_ThenAnErrorIsReturned() { let mockPrompt = MockChromiumPrompt(returnValue: .userDeniedKeychainPrompt) let reader = ChromiumLoginReader( @@ -90,16 +90,16 @@ class ChromiumLoginReaderTests: XCTestCase { processName: "Chrome", decryptionKeyPrompt: mockPrompt ) - + let result = reader.readLogins() - + if case let .failure(type) = result { XCTAssertEqual(type, .userDeniedKeychainPrompt) } else { XCTFail("Received unexpected success") } } - + func testWhenImportingChromiumData_AndTheKeychainCausesAnError_ThenTheStatusCodeIsReturned() { let mockPrompt = MockChromiumPrompt(returnValue: .keychainError(123)) let reader = ChromiumLoginReader( @@ -107,22 +107,22 @@ class ChromiumLoginReaderTests: XCTestCase { processName: "Chrome", decryptionKeyPrompt: mockPrompt ) - + let result = reader.readLogins() - + if case let .failure(type) = result { XCTAssertEqual(type, .decryptionKeyAccessFailed(123)) } else { XCTFail("Received unexpected success") } } - + } private class MockChromiumPrompt: ChromiumKeychainPrompting { - + var returnValue: ChromiumKeychainPromptResult - + init(returnValue: ChromiumKeychainPromptResult) { self.returnValue = returnValue } @@ -130,5 +130,5 @@ private class MockChromiumPrompt: ChromiumKeychainPrompting { func promptForChromiumPasswordKeychainAccess(processName: String) -> ChromiumKeychainPromptResult { returnValue } - + } diff --git a/Unit Tests/Data Import/FirefoxBookmarksReaderTests.swift b/Unit Tests/Data Import/FirefoxBookmarksReaderTests.swift index d5e8f94973..4d4eedb372 100644 --- a/Unit Tests/Data Import/FirefoxBookmarksReaderTests.swift +++ b/Unit Tests/Data Import/FirefoxBookmarksReaderTests.swift @@ -33,7 +33,7 @@ class FirefoxBookmarksReaderTests: XCTestCase { XCTAssertEqual(bookmarks.topLevelFolders.bookmarkBar.type, "folder") XCTAssertEqual(bookmarks.topLevelFolders.otherBookmarks.type, "folder") - + XCTAssertTrue(bookmarks.topLevelFolders.bookmarkBar.children!.contains(where: { bookmark in bookmark.url?.absoluteString == "https://duckduckgo.com/" })) diff --git a/Unit Tests/Data Import/FirefoxDataImporterTests.swift b/Unit Tests/Data Import/FirefoxDataImporterTests.swift index 2144349abc..a31940e924 100644 --- a/Unit Tests/Data Import/FirefoxDataImporterTests.swift +++ b/Unit Tests/Data Import/FirefoxDataImporterTests.swift @@ -21,30 +21,30 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser class FirefoxDataImporterTests: XCTestCase { - + func testWhenImportingWithoutAnyDataTypes_ThenSummaryIsEmpty() async { let loginImporter = MockLoginImporter() let faviconManager = FaviconManagerMock() let bookmarkImporter = MockBookmarkImporter(importBookmarks: { _, _ in .init(successful: 0, duplicates: 0, failed: 0) }) let importer = FirefoxDataImporter(loginImporter: loginImporter, bookmarkImporter: bookmarkImporter, faviconManager: faviconManager) - + let summary = await importer.importData(types: [], from: .init(profileURL: resourceURL())) - + if case let .success(summary) = summary { XCTAssert(summary.isEmpty) } else { XCTFail("Received failure unexpectedly") } } - + func testWhenImportingBookmarks_AndBookmarkImportSucceeds_ThenSummaryIsPopulated() async { let loginImporter = MockLoginImporter() let faviconManager = FaviconManagerMock() let bookmarkImporter = MockBookmarkImporter(importBookmarks: { _, _ in .init(successful: 1, duplicates: 2, failed: 3) }) let importer = FirefoxDataImporter(loginImporter: loginImporter, bookmarkImporter: bookmarkImporter, faviconManager: faviconManager) - + let summary = await importer.importData(types: [.bookmarks], from: .init(profileURL: resourceURL())) - + if case let .success(summary) = summary { XCTAssertEqual(summary.bookmarksResult?.successful, 1) XCTAssertEqual(summary.bookmarksResult?.duplicates, 2) @@ -61,16 +61,16 @@ class FirefoxDataImporterTests: XCTestCase { let bookmarkImporter = MockBookmarkImporter(throwableError: DataImportError.bookmarks(.cannotAccessCoreData), importBookmarks: { _, _ in .init(successful: 0, duplicates: 0, failed: 0) }) let importer = FirefoxDataImporter(loginImporter: loginImporter, bookmarkImporter: bookmarkImporter, faviconManager: faviconManager) - + let summary = await importer.importData(types: [.bookmarks], from: .init(profileURL: resourceURL())) - + if case let .failure(error) = summary { XCTAssertEqual(error, .bookmarks(.cannotReadFile)) } else { XCTFail("Received summary unexpectedly") } } - + private func resourceURL() -> URL { let bundle = Bundle(for: FirefoxBookmarksReaderTests.self) return bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data") diff --git a/Unit Tests/Data Import/FirefoxKeyReaderTests.swift b/Unit Tests/Data Import/FirefoxKeyReaderTests.swift index 2190ba674f..fe86d45faf 100644 --- a/Unit Tests/Data Import/FirefoxKeyReaderTests.swift +++ b/Unit Tests/Data Import/FirefoxKeyReaderTests.swift @@ -22,48 +22,48 @@ import XCTest import CryptoKit class FirefoxKeyReaderTests: XCTestCase { - + func testWhenReadingValidKey3Database_AndNoPrimaryPasswordIsSet_ThenKeyIsRead() { let databaseURL = resourcesURLWithoutPassword().appendingPathComponent("key3-firefox46.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key3DatabaseURL: databaseURL, primaryPassword: "") - + if case let .success(data) = result { XCTAssertEqual(data.count, 24) } else { XCTFail("Failed to read decryption key") } } - + func testWhenReadingValidKey3Database_AndPrimaryPasswordIsSet_AndPrimaryPasswordIsValid_ThenKeyIsRead() { let databaseURL = resourcesURLWithPassword().appendingPathComponent("key3-firefox46.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key3DatabaseURL: databaseURL, primaryPassword: "сЮЛОажс$4vz*VçàhxpfCbmwo") - + if case let .success(data) = result { XCTAssertEqual(data.count, 24) } else { XCTFail("Failed to read decryption key") } } - + func testWhenReadingValidKey3Database_AndPrimaryPasswordIsSet_AndPrimaryPasswordIsInvalid_ThenKeyIsNotRead() { let databaseURL = resourcesURLWithPassword().appendingPathComponent("key3-firefox46.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key3DatabaseURL: databaseURL, primaryPassword: "invalid-password") - + if case let .failure(error) = result { XCTAssertEqual(error, .decryptionFailed) } else { XCTFail("Did not expect to get valid decryption key") } } - + func testWhenReadingInvalidKey3Database_ThenKeyIsNotRead() { let databaseURL = resourcesURLWithoutPassword().appendingPathComponent("key3-firefox46-broken.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key3DatabaseURL: databaseURL, primaryPassword: "") - + if case let .failure(error) = result { XCTAssertEqual(error, FirefoxLoginReader.ImportError.decryptionFailed) } else { @@ -75,58 +75,58 @@ class FirefoxKeyReaderTests: XCTestCase { let databaseURL = resourcesURLWithoutPassword().appendingPathComponent("key4.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key4DatabaseURL: databaseURL, primaryPassword: "") - + if case let .success(data) = result { XCTAssertEqual(data.count, 24) } else { XCTFail("Failed to read decryption key") } } - + func testFirefox59_WhenReadingValidKey4Database_AndNoPrimaryPasswordIsSet_ThenKeyIsRead() { let databaseURL = resourcesURLWithoutPassword().appendingPathComponent("key4-firefox59.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key4DatabaseURL: databaseURL, primaryPassword: "") - + if case let .success(data) = result { XCTAssertEqual(data.count, 24) } else { XCTFail("Failed to read decryption key") } } - + func testWhenReadingValidKey4Database_AndPrimaryPasswordIsProvided_ThenKeyIsRead() { let databaseURL = resourcesURLWithPassword().appendingPathComponent("key4-encrypted.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key4DatabaseURL: databaseURL, primaryPassword: "testpassword") - + if case let .success(data) = result { XCTAssertEqual(data.count, 24) } else { XCTFail("Failed to read decryption key") } } - + func testWhenReadingValidKey4Database_AndPrimaryPasswordIsNotProvided_ThenKeyIsNotRead() { let databaseURL = resourcesURLWithPassword().appendingPathComponent("key4-encrypted.db") let reader = FirefoxEncryptionKeyReader() let result = reader.getEncryptionKey(key4DatabaseURL: databaseURL, primaryPassword: "") - + if case let .failure(error) = result { XCTAssertEqual(error, .requiresPrimaryPassword) } else { XCTFail("Failed to read decryption key") } } - + private func resourcesURLWithPassword() -> URL { let bundle = Bundle(for: FirefoxLoginReaderTests.self) return bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data/Primary Password") } - + private func resourcesURLWithoutPassword() -> URL { let bundle = Bundle(for: FirefoxLoginReaderTests.self) return bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data/No Primary Password") } - + } diff --git a/Unit Tests/Data Import/FirefoxLoginReaderTests.swift b/Unit Tests/Data Import/FirefoxLoginReaderTests.swift index 7d6b655124..ce660d7484 100644 --- a/Unit Tests/Data Import/FirefoxLoginReaderTests.swift +++ b/Unit Tests/Data Import/FirefoxLoginReaderTests.swift @@ -23,19 +23,19 @@ import XCTest class FirefoxLoginReaderTests: XCTestCase { private let rootDirectoryName = UUID().uuidString - + func testWhenImportingFirefox46LoginsWithNoPrimaryPassword_ThenImportSucceeds() throws { let database = resourcesURLWithoutPassword().appendingPathComponent("key3-firefox46.db") let logins = resourcesURLWithoutPassword().appendingPathComponent("logins-firefox46.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key3.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL) let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -44,22 +44,22 @@ class FirefoxLoginReaderTests: XCTestCase { } else { XCTFail("Failed to decrypt Firefox logins") } - + try structure.removeCreatedFileSystemStructure() } - + func testWhenImportingLoginsWithNoPrimaryPassword_ThenImportSucceeds() throws { let database = resourcesURLWithoutPassword().appendingPathComponent("key4.db") let logins = resourcesURLWithoutPassword().appendingPathComponent("logins.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL) let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -68,22 +68,22 @@ class FirefoxLoginReaderTests: XCTestCase { } else { XCTFail("Failed to decrypt Firefox logins") } - + try structure.removeCreatedFileSystemStructure() } func testWhenImportingLoginsWithPrimaryPassword_AndNoPrimaryPasswordIsProvided_ThenImportFails() throws { let database = resourcesURLWithPassword().appendingPathComponent("key4-encrypted.db") let logins = resourcesURLWithPassword().appendingPathComponent("logins-encrypted.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL) let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -92,22 +92,22 @@ class FirefoxLoginReaderTests: XCTestCase { } else { XCTFail("Expected to fail when decrypting a database that is protected with a Primary Password") } - + try structure.removeCreatedFileSystemStructure() } func testWhenImportingLoginsWithPrimaryPassword_AndPrimaryPasswordIsProvided_ThenImportSucceeds() throws { let database = resourcesURLWithPassword().appendingPathComponent("key4-encrypted.db") let logins = resourcesURLWithPassword().appendingPathComponent("logins-encrypted.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "testpassword") let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -117,39 +117,39 @@ class FirefoxLoginReaderTests: XCTestCase { XCTFail("Failed to decrypt Firefox logins") } } - + func testWhenImportingLoginsFromADirectory_AndNoMatchingFilesAreFound_ThenImportFails() throws { let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("unrelated-file", contents: .string("")) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL) let result = firefoxLoginReader.readLogins(dataFormat: nil) - + if case let .failure(error) = result { XCTAssertEqual(error, .couldNotFindLoginsFile) } else { XCTFail("Expected to fail when decrypting a database that is protected with a Primary Password") } - + try structure.removeCreatedFileSystemStructure() } - + func testWhenImportingFirefox70LoginsWithNoPrimaryPassword_ThenImportSucceeds() throws { let database = resourcesURLWithoutPassword().appendingPathComponent("key4-firefox70.db") let logins = resourcesURLWithoutPassword().appendingPathComponent("logins-firefox70.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "") let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -159,19 +159,19 @@ class FirefoxLoginReaderTests: XCTestCase { XCTFail("Failed to decrypt Firefox logins") } } - + func testWhenImportingFirefox70LoginsWithPrimaryPassword_AndPrimaryPasswordIsProvided_ThenImportSucceeds() throws { let database = resourcesURLWithPassword().appendingPathComponent("key4-firefox70.db") let logins = resourcesURLWithPassword().appendingPathComponent("logins-firefox70.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "test") let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -181,19 +181,19 @@ class FirefoxLoginReaderTests: XCTestCase { XCTFail("Failed to decrypt Firefox logins") } } - + func testWhenImportingFirefox70LoginsWithPrimaryPassword_AndNoPrimaryPasswordIsProvided_ThenImportFails() throws { let database = resourcesURLWithPassword().appendingPathComponent("key4-firefox70.db") let logins = resourcesURLWithPassword().appendingPathComponent("logins-firefox70.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "") let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -203,19 +203,19 @@ class FirefoxLoginReaderTests: XCTestCase { XCTFail("Did not expect decryption success") } } - + func testWhenImportingFirefox84LoginsWithNoPrimaryPassword_ThenImportSucceeds() throws { let database = resourcesURLWithoutPassword().appendingPathComponent("key4-firefox84.db") let logins = resourcesURLWithoutPassword().appendingPathComponent("logins-firefox84.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "") let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -225,19 +225,19 @@ class FirefoxLoginReaderTests: XCTestCase { XCTFail("Failed to decrypt Firefox logins") } } - + func testWhenImportingFirefox84LoginsWithPrimaryPassword_AndPrimaryPasswordIsProvided_ThenImportSucceeds() throws { let database = resourcesURLWithPassword().appendingPathComponent("key4-firefox84.db") let logins = resourcesURLWithPassword().appendingPathComponent("logins-firefox84.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "test") let result = firefoxLoginReader.readLogins(dataFormat: nil) @@ -247,19 +247,19 @@ class FirefoxLoginReaderTests: XCTestCase { XCTFail("Failed to decrypt Firefox logins") } } - + func testWhenImportingFirefox84LoginsWithPrimaryPassword_AndNoPrimaryPasswordIsProvided_ThenImportFails() throws { let database = resourcesURLWithPassword().appendingPathComponent("key4-firefox84.db") let logins = resourcesURLWithPassword().appendingPathComponent("logins-firefox84.json") - + let structure = FileSystem(rootDirectoryName: rootDirectoryName) { File("key4.db", contents: .copy(database)) File("logins.json", contents: .copy(logins)) } - + try structure.writeToTemporaryDirectory() let profileDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(rootDirectoryName) - + let firefoxLoginReader = FirefoxLoginReader(firefoxProfileURL: profileDirectoryURL, primaryPassword: "") let result = firefoxLoginReader.readLogins(dataFormat: nil) diff --git a/Unit Tests/Data Import/ThirdPartyBrowserTests.swift b/Unit Tests/Data Import/ThirdPartyBrowserTests.swift index 9b6d2212d8..b2d2b6ae32 100644 --- a/Unit Tests/Data Import/ThirdPartyBrowserTests.swift +++ b/Unit Tests/Data Import/ThirdPartyBrowserTests.swift @@ -23,13 +23,13 @@ import XCTest class ThirdPartyBrowserTests: XCTestCase { private let mockApplicationSupportDirectoryName = UUID().uuidString - + override func setUp() { super.setUp() let defaultRootDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(mockApplicationSupportDirectoryName) try? FileManager.default.removeItem(at: defaultRootDirectoryURL) } - + override func tearDown() { super.tearDown() let defaultRootDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(mockApplicationSupportDirectoryName) @@ -44,18 +44,18 @@ class ThirdPartyBrowserTests: XCTestCase { XCTAssertNotNil(ThirdPartyBrowser.browser(for: .lastPass)) XCTAssertNotNil(ThirdPartyBrowser.browser(for: .onePassword)) XCTAssertNotNil(ThirdPartyBrowser.browser(for: .safari)) - + XCTAssertNil(ThirdPartyBrowser.browser(for: .csv)) } func testWhenCreatingThirdPartyBrowser_AndValidBrowserIsNotProvided_ThenThirdPartyBrowserInitializationFails() { XCTAssertNil(ThirdPartyBrowser.browser(for: .csv)) } - + func testWhenGettingBrowserProfiles_AndFirefoxProfileExists_ThenFirefoxProfileIsReturned() throws { let defaultProfileName = "o20p2fk2.default" let defaultReleaseProfileName = "9q0lq57x.default-release" - + let mockApplicationSupportDirectory = FileSystem(rootDirectoryName: mockApplicationSupportDirectoryName) { Directory("Firefox") { Directory("Profiles") { @@ -63,7 +63,7 @@ class ThirdPartyBrowserTests: XCTestCase { File("key4.db", contents: .copy(key4DatabaseURL())) File("logins.json", contents: .copy(loginsURL())) } - + Directory(defaultProfileName) { File("key4.db", contents: .copy(key4DatabaseURL())) File("logins.json", contents: .copy(loginsURL())) @@ -71,10 +71,10 @@ class ThirdPartyBrowserTests: XCTestCase { } } } - + let mockApplicationSupportDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(mockApplicationSupportDirectoryName) try mockApplicationSupportDirectory.writeToTemporaryDirectory() - + guard let list = ThirdPartyBrowser.firefox.browserProfiles(supportDirectoryURL: mockApplicationSupportDirectoryURL) else { XCTFail("Failed to get profile list") return @@ -84,12 +84,12 @@ class ThirdPartyBrowserTests: XCTestCase { XCTAssertEqual(list.defaultProfile?.profileName, "default-release") XCTAssertTrue(list.defaultProfile?.hasLoginData ?? false) } - + private func key4DatabaseURL() -> URL { let bundle = Bundle(for: ThirdPartyBrowserTests.self) return bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data/No Primary Password/key4.db") } - + private func loginsURL() -> URL { let bundle = Bundle(for: ThirdPartyBrowserTests.self) return bundle.resourceURL!.appendingPathComponent("Data Import Resources/Test Firefox Data/No Primary Password/logins.json") diff --git a/Unit Tests/File Download/DownloadListCoordinatorTests.swift b/Unit Tests/File Download/DownloadListCoordinatorTests.swift index ad2e6d5427..420480ced5 100644 --- a/Unit Tests/File Download/DownloadListCoordinatorTests.swift +++ b/Unit Tests/File Download/DownloadListCoordinatorTests.swift @@ -60,7 +60,7 @@ final class DownloadListCoordinatorTests: XCTestCase { } } - func setUpCoordinatorAndAddDownload() -> (WKDownloadMock, WebKitDownloadTask, UUID) { // swiftlint:disable:this large_tuple + func setUpCoordinatorAndAddDownload() -> (WKDownloadMock, WebKitDownloadTask, UUID) { setUpCoordinator() let download = WKDownloadMock() let task = WebKitDownloadTask(download: download, promptForLocation: false, destinationURL: destURL, tempURL: tempURL) @@ -407,7 +407,7 @@ final class DownloadListCoordinatorTests: XCTestCase { } coordinator.cleanupInactiveDownloads() - + waitForExpectations(timeout: 1) XCTAssertTrue(coordinator.hasActiveDownloads) XCTAssertEqual(coordinator.downloads(sortedBy: \.modified, ascending: true).count, 1) diff --git a/Unit Tests/File Download/FileDownloadManagerMock.swift b/Unit Tests/File Download/FileDownloadManagerMock.swift index 7f08dd205c..7ade773a6a 100644 --- a/Unit Tests/File Download/FileDownloadManagerMock.swift +++ b/Unit Tests/File Download/FileDownloadManagerMock.swift @@ -23,7 +23,7 @@ import Combine final class FileDownloadManagerMock: FileDownloadManagerProtocol { var downloads = Set() - + var downloadAddedSubject = PassthroughSubject() var downloadsPublisher: AnyPublisher { downloadAddedSubject.eraseToAnyPublisher() diff --git a/Unit Tests/Geolocation/CLLocationManagerMock.swift b/Unit Tests/Geolocation/CLLocationManagerMock.swift index 8d7a2e31a0..371308aab6 100644 --- a/Unit Tests/Geolocation/CLLocationManagerMock.swift +++ b/Unit Tests/Geolocation/CLLocationManagerMock.swift @@ -78,7 +78,7 @@ final class CLLocationManagerMock: CLLocationManager { } } } - + override var location: CLLocation? { currentLocation } diff --git a/Unit Tests/Geolocation/GeolocationProviderTests.swift b/Unit Tests/Geolocation/GeolocationProviderTests.swift index 92c65d4091..9a3fa48e10 100644 --- a/Unit Tests/Geolocation/GeolocationProviderTests.swift +++ b/Unit Tests/Geolocation/GeolocationProviderTests.swift @@ -23,7 +23,6 @@ import CoreLocation import WebKit @testable import DuckDuckGo_Privacy_Browser -// swiftlint:disable type_body_length final class GeolocationProviderTests: XCTestCase { let geolocationServiceMock = GeolocationServiceMock() @@ -279,12 +278,10 @@ final class GeolocationProviderTests: XCTestCase { } } - // swiftlint:disable identifier_name let e1_1 = expectation(description: "location1 received in webView1") let e1_2 = expectation(description: "location1 received in webView2") let e2_1 = expectation(description: "location2 received in webView1") let e2_2 = expectation(description: "location2 received in webView2") - // swiftlint:enable identifier_name geolocationHandler = { [webView1=webView!] webView, body in switch (webView, try Response(body)) { @@ -403,11 +400,9 @@ final class GeolocationProviderTests: XCTestCase { } } - // swiftlint:disable identifier_name let e1_1 = expectation(description: "location1 received in webView1") let e1_2 = expectation(description: "location1 received in webView2") let e2_1 = expectation(description: "location2 received in webView1") - // swiftlint:enable identifier_name geolocationHandler = { [webView1=webView!] webView, body in switch (webView, try Response(body)) { @@ -549,7 +544,7 @@ final class GeolocationProviderTests: XCTestCase { webView.loadHTMLString(Self.getCurrentPosition, baseURL: .duckDuckGo) NSApp.activate(ignoringOtherApps: true) webView.window?.orderFrontRegardless() - + waitForExpectations(timeout: 5) } @@ -653,7 +648,6 @@ final class GeolocationProviderTests: XCTestCase { } } -// swiftlint:enable function_body_length extension GeolocationProviderTests: WKUIDelegate { @objc(_webView:requestGeolocationPermissionForFrame:decisionHandler:) @@ -675,7 +669,7 @@ extension GeolocationProviderTests: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { let webView = webViews.first(where: { $0.configuration.userContentController === userContentController })! XCTAssertNoThrow(try geolocationHandler?(webView, message.body)) - + } } diff --git a/Unit Tests/Geolocation/GeolocationServiceTests.swift b/Unit Tests/Geolocation/GeolocationServiceTests.swift index 0cbb32b141..af0e5fe74a 100644 --- a/Unit Tests/Geolocation/GeolocationServiceTests.swift +++ b/Unit Tests/Geolocation/GeolocationServiceTests.swift @@ -135,8 +135,6 @@ final class GeolocationServiceTests: XCTestCase { } } - // swiftlint:disable cyclomatic_complexity - // swiftlint:disable function_body_length func testWhenManySubscribedThenLocationIsPublished() { let location = CLLocation(latitude: 51.1, longitude: 23.4) struct TestError: Error {} @@ -196,8 +194,6 @@ final class GeolocationServiceTests: XCTestCase { waitForExpectations(timeout: 1) } } - // swiftlint:enable cyclomatic_complexity - // swiftlint:enable function_body_length func testWhenLastSubscriberUnsubscribedThenLocationManagerStopped() { let c1 = service.locationPublisher.sink { _ in } diff --git a/Unit Tests/History/Model/HistoryCoordinatorTests.swift b/Unit Tests/History/Model/HistoryCoordinatorTests.swift index da554d733b..9021de5bdb 100644 --- a/Unit Tests/History/Model/HistoryCoordinatorTests.swift +++ b/Unit Tests/History/Model/HistoryCoordinatorTests.swift @@ -108,7 +108,7 @@ class HistoryCoordinatorTests: XCTestCase { func testWhenHistoryIsBurning_ThenHistoryIsCleanedExceptFireproofDomains() { let (historyStoringMock, historyCoordinator) = HistoryCoordinator.aHistoryCoordinator - + let url1 = URL(string: "https://duckduckgo.com")! historyCoordinator.addVisit(of: url1) @@ -130,19 +130,19 @@ class HistoryCoordinatorTests: XCTestCase { XCTAssert(historyStoringMock.removeEntriesArray.count == 3) } } - + func testWhenBurningVisits_removesHistoryWhenVisitsCountHitsZero() { let (historyStoringMock, historyCoordinator) = HistoryCoordinator.aHistoryCoordinator historyStoringMock.removeEntriesResult = .success(()) historyStoringMock.removeVisitsResult = .success(()) - + let url1 = URL(string: "https://duckduckgo.com")! historyCoordinator.addVisit(of: url1) historyCoordinator.addVisit(of: url1) historyCoordinator.addVisit(of: url1) - + let visitsToBurn = Array(historyCoordinator.history!.first!.visits) - + let waiter = expectation(description: "Wait") historyCoordinator.burnVisits(visitsToBurn) { waiter.fulfill() @@ -151,19 +151,19 @@ class HistoryCoordinatorTests: XCTestCase { } waitForExpectations(timeout: 1.0) } - + func testWhenBurningVisits_removesVisitsFromTheStore() { let (historyStoringMock, historyCoordinator) = HistoryCoordinator.aHistoryCoordinator historyStoringMock.removeEntriesResult = .success(()) historyStoringMock.removeVisitsResult = .success(()) - + let url1 = URL(string: "https://duckduckgo.com")! historyCoordinator.addVisit(of: url1) historyCoordinator.addVisit(of: url1) historyCoordinator.addVisit(of: url1) - + let visitsToBurn = Array(historyCoordinator.history!.first!.visits) - + let waiter = expectation(description: "Wait") historyCoordinator.burnVisits(visitsToBurn) { waiter.fulfill() @@ -171,13 +171,13 @@ class HistoryCoordinatorTests: XCTestCase { } waitForExpectations(timeout: 1.0) } - + func testWhenBurningVisits_DoesntDeleteHistoryBeforeVisits() { // Needs real store to catch assertion which can be raised by improper call ordering in the coordinator let context = CoreData.historyStoreContainer().newBackgroundContext() let historyStore = HistoryStore(context: context) let historyCoordinator = HistoryCoordinator(historyStoring: historyStore) - + let url1 = URL(string: "https://duckduckgo.com")! historyCoordinator.addVisit(of: url1) historyCoordinator.addVisit(of: url1) @@ -187,9 +187,9 @@ class HistoryCoordinatorTests: XCTestCase { historyCoordinator.addVisit(of: url2) historyCoordinator.addVisit(of: url2) historyCoordinator.addVisit(of: url2) - + let visitsToBurn = Array(historyCoordinator.history!.first!.visits) - + let waiter = expectation(description: "Wait") historyCoordinator.burnVisits(visitsToBurn) { waiter.fulfill() diff --git a/Unit Tests/History/Services/HistoryStoreTests.swift b/Unit Tests/History/Services/HistoryStoreTests.swift index 667d4a3810..3101f14533 100644 --- a/Unit Tests/History/Services/HistoryStoreTests.swift +++ b/Unit Tests/History/Services/HistoryStoreTests.swift @@ -24,11 +24,11 @@ import class Persistence.CoreDataDatabase final class HistoryStoreTests: XCTestCase { private var cancellables = Set() - + private var context: NSManagedObjectContext! private var historyStore: HistoryStore! private var location: URL! - + override func setUp() { super.setUp() let model = CoreDataDatabase.loadModel(from: .main, named: "History")! @@ -42,7 +42,7 @@ final class HistoryStoreTests: XCTestCase { context = database.makeContext(concurrencyType: .mainQueueConcurrencyType) historyStore = HistoryStore(context: context) } - + override func tearDownWithError() throws { try FileManager.default.removeItem(at: location) context = nil @@ -82,7 +82,7 @@ final class HistoryStoreTests: XCTestCase { visits: []) let savingExpectation = self.expectation(description: "Saving") save(entry: newHistoryEntry, expectation: savingExpectation) - + var toBeDeleted: [HistoryEntry] = [] for i in 0..<150 { let identifier = UUID() @@ -128,7 +128,7 @@ final class HistoryStoreTests: XCTestCase { } removeEntriesAndWait(toBeDeleted) - + context.performAndWait { let request = DuckDuckGo_Privacy_Browser.HistoryEntryManagedObject.fetchRequest() do { @@ -140,7 +140,7 @@ final class HistoryStoreTests: XCTestCase { } } } - + func removeEntriesAndWait(_ entries: [HistoryEntry], file: StaticString = #file, line: UInt = #line) { let loadingExpectation = self.expectation(description: "Loading") historyStore.removeEntries(entries) @@ -157,7 +157,7 @@ final class HistoryStoreTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - + func testWhenRemoveEntriesIsCalled_visitsCascadeDelete() { var toBeDeleted = [Visit]() for j in 0..<10 { @@ -166,9 +166,9 @@ final class HistoryStoreTests: XCTestCase { toBeDeleted.append(visit) } let history = saveNewHistoryEntry(including: toBeDeleted, lastVisit: toBeDeleted.last!.date) - + removeEntriesAndWait([history]) - + context.performAndWait { let request = DuckDuckGo_Privacy_Browser.VisitManagedObject.fetchRequest() do { @@ -179,7 +179,7 @@ final class HistoryStoreTests: XCTestCase { } } } - + func testWhenRemoveVisitsIsCalled_ThenVisitsMustBeCleaned() { let visitDate = Date(timeIntervalSince1970: 1234) let toBeKept = Visit(date: visitDate) @@ -193,7 +193,7 @@ final class HistoryStoreTests: XCTestCase { let history = self.saveNewHistoryEntry(including: visits, lastVisit: visits.last!.date) historiesToPreventFromDeallocation.append(history) } - + for _ in 0..<3 { var visits = [Visit]() for j in 0..<50 { @@ -221,7 +221,7 @@ final class HistoryStoreTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - + context.performAndWait { let request = DuckDuckGo_Privacy_Browser.VisitManagedObject.fetchRequest() do { @@ -234,11 +234,11 @@ final class HistoryStoreTests: XCTestCase { } } } - + func testWhenCleanOldIsCalled_ThenFollowingSaveShouldSucceed() { let oldVisitDate = Date(timeIntervalSince1970: 0) let newVisitDate = Date(timeIntervalSince1970: 12345) - + let oldVisit = Visit(date: oldVisitDate) let newVisit = Visit(date: newVisitDate) @@ -246,7 +246,7 @@ final class HistoryStoreTests: XCTestCase { saveNewHistoryEntry(including: [oldVisit, newVisit], lastVisit: newVisitDate, expectation: firstSavingExpectation) - + cleanOldAndWait(cleanUntil: Date(timeIntervalSince1970: 1)) { history in XCTAssertEqual(history.count, 1) for entry in history { @@ -259,14 +259,14 @@ final class HistoryStoreTests: XCTestCase { saveNewHistoryEntry(including: [oldVisit, newVisit], lastVisit: newVisitDate, expectation: secondSavingExpectation) - + waitForExpectations(timeout: 2, handler: nil) } - + func testWhenRemoveVisitsIsCalled_ThenFollowingSaveShouldSucceed() { let oldVisitDate = Date(timeIntervalSince1970: 0) let newVisitDate = Date(timeIntervalSince1970: 12345) - + let oldVisit = Visit(date: oldVisitDate) let newVisit = Visit(date: newVisitDate) @@ -291,19 +291,19 @@ final class HistoryStoreTests: XCTestCase { .store(in: &cancellables) waitForExpectations(timeout: 2, handler: nil) } - + let secondSavingExpectation = self.expectation(description: "Saving") saveNewHistoryEntry(including: [oldVisit, newVisit], lastVisit: newVisitDate, expectation: secondSavingExpectation) - + waitForExpectations(timeout: 2, handler: nil) } - + func testWhenRemoveEntriesIsCalled_ThenFollowingSaveShouldSucceed() { let oldVisitDate = Date(timeIntervalSince1970: 0) let newVisitDate = Date(timeIntervalSince1970: 12345) - + let oldVisit = Visit(date: oldVisitDate) let newVisit = Visit(date: newVisitDate) @@ -311,17 +311,17 @@ final class HistoryStoreTests: XCTestCase { let historyEntry = saveNewHistoryEntry(including: [oldVisit, newVisit], lastVisit: newVisitDate, expectation: firstSavingExpectation) - + removeEntriesAndWait([historyEntry]) - + let secondSavingExpectation = self.expectation(description: "Saving") saveNewHistoryEntry(including: [oldVisit, newVisit], lastVisit: newVisitDate, expectation: secondSavingExpectation) - + waitForExpectations(timeout: 2, handler: nil) } - + private func cleanOldAndWait(cleanUntil date: Date, assertion: @escaping (History) -> Void, file: StaticString = #file, line: UInt = #line) { let loadingExpectation = self.expectation(description: "Loading") historyStore.cleanOld(until: date) @@ -338,7 +338,7 @@ final class HistoryStoreTests: XCTestCase { waitForExpectations(timeout: 2, handler: nil) } - + @discardableResult private func saveNewHistoryEntry(including visits: [Visit], lastVisit: Date, expectation: XCTestExpectation? = nil, file: StaticString = #file, line: UInt = #line) -> HistoryEntry { let historyEntry = HistoryEntry(identifier: UUID(), diff --git a/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift b/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift index 47a2400fa6..16a2a2ffa9 100644 --- a/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift +++ b/Unit Tests/Home Page/RecentlyVisitedSiteModelTests.swift @@ -21,7 +21,6 @@ import XCTest class RecentlyVisitedSiteModelTests: XCTestCase { - // swiftlint:disable:next identifier_name private func RecentlyVisitedSiteModel(originalURL: URL, privatePlayer: PrivatePlayerMode = .disabled) -> HomePage.Models.RecentlyVisitedSiteModel? { HomePage.Models.RecentlyVisitedSiteModel(originalURL: originalURL, bookmarkManager: LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(), faviconManagement: FaviconManagerMock()), fireproofDomains: FireproofDomains(store: FireproofDomainsStoreMock()), privatePlayer: .mock(withMode: privatePlayer)) } @@ -29,16 +28,16 @@ class RecentlyVisitedSiteModelTests: XCTestCase { func testWhenOriginalURLIsHTTPS_ThenModelURLIsHTTPS() { assertModelWithURL(URL(string: "https://example.com")!, matches: URL(string: "https://example.com")!, expectedDomain: "example.com") } - + func testWhenOriginalURLIsHTTP_ThenModelURLIsHTTP() { assertModelWithURL(URL(string: "http://example.com")!, matches: URL(string: "http://example.com")!, expectedDomain: "example.com") } - + func testWhenOriginalURLContainsAdditionalInformation_ThenModelURLOnlyUsesSchemeAndHost() { assertModelWithURL(URL(string: "http://example.com/path?test=true#fragment")!, matches: URL(string: "http://example.com")!, expectedDomain: "example.com") assertModelWithURL(URL(string: "https://example.com/path?test=true#fragment")!, matches: URL(string: "https://example.com")!, expectedDomain: "example.com") } - + func testWhenOriginalURLContainsWWW_ThenDomainDoesNotIncludeIt() { assertModelWithURL(URL(string: "http://www.example.com")!, matches: URL(string: "http://www.example.com")!, expectedDomain: "example.com") } diff --git a/Unit Tests/Menus/MainMenuTests.swift b/Unit Tests/Menus/MainMenuTests.swift index 54ecb6870e..44ec9cdb11 100644 --- a/Unit Tests/Menus/MainMenuTests.swift +++ b/Unit Tests/Menus/MainMenuTests.swift @@ -26,11 +26,9 @@ class MainMenuTests: XCTestCase { @Published var isInInitialState = true - // swiftlint:disable implicitly_unwrapped_optional var lastSessionMenuItem: NSMenuItem! var lastTabMenuItem: NSMenuItem! var manager: ReopenMenuItemKeyEquivalentManager! - // swiftlint:enable implicitly_unwrapped_optional override func setUpWithError() throws { isInInitialState = true diff --git a/Unit Tests/Navigation Bar/LocalPinningManagerTests.swift b/Unit Tests/Navigation Bar/LocalPinningManagerTests.swift index 002a8d3f89..0a51250951 100644 --- a/Unit Tests/Navigation Bar/LocalPinningManagerTests.swift +++ b/Unit Tests/Navigation Bar/LocalPinningManagerTests.swift @@ -25,47 +25,47 @@ final class LocalPinningManagerTests: XCTestCase { super.setUp() UserDefaultsWrapper.clearAll() } - + override func tearDown() { super.tearDown() UserDefaultsWrapper.clearAll() } - + func testWhenTogglingPinningForAView_AndViewIsNotPinned_ThenViewBecomesPinned() { let manager = LocalPinningManager() - + XCTAssertFalse(manager.isPinned(.autofill)) XCTAssertFalse(manager.isPinned(.bookmarks)) - + manager.togglePinning(for: .autofill) - + XCTAssertTrue(manager.isPinned(.autofill)) XCTAssertFalse(manager.isPinned(.bookmarks)) } - + func testWhenTogglingPinningForAView_AndViewIsAlreadyPinned_ThenViewBecomesUnpinned() { let manager = LocalPinningManager() - + XCTAssertFalse(manager.isPinned(.autofill)) XCTAssertFalse(manager.isPinned(.bookmarks)) - + manager.togglePinning(for: .autofill) - + XCTAssertTrue(manager.isPinned(.autofill)) XCTAssertFalse(manager.isPinned(.bookmarks)) - + manager.togglePinning(for: .autofill) - + XCTAssertFalse(manager.isPinned(.autofill)) XCTAssertFalse(manager.isPinned(.bookmarks)) } - + func testWhenChangingPinnedViews_ThenNotificationIsPosted() { expectation(forNotification: .PinnedViewsChanged, object: nil) - + let manager = LocalPinningManager() manager.togglePinning(for: .autofill) - + waitForExpectations(timeout: 1.0) } diff --git a/Unit Tests/Onboarding/OnboardingTests.swift b/Unit Tests/Onboarding/OnboardingTests.swift index 110e9cbbb1..3ece7679ae 100644 --- a/Unit Tests/Onboarding/OnboardingTests.swift +++ b/Unit Tests/Onboarding/OnboardingTests.swift @@ -21,9 +21,8 @@ import XCTest class OnboardingTests: XCTestCase { - // swiftlint:disable weak_delegate + // swiftlint:disable:next weak_delegate let delegate = MockOnboardingDelegate() - // swiftlint:enable weak_delegate @UserDefaultsWrapper(key: .onboardingFinished, defaultValue: false) var onboardingFinished: Bool diff --git a/Unit Tests/Permissions/PermissionModelTests.swift b/Unit Tests/Permissions/PermissionModelTests.swift index cda3fa6ece..bcca95d4de 100644 --- a/Unit Tests/Permissions/PermissionModelTests.swift +++ b/Unit Tests/Permissions/PermissionModelTests.swift @@ -23,7 +23,6 @@ import Combine import AVFoundation @testable import DuckDuckGo_Privacy_Browser -// swiftlint:disable:next type_body_length final class PermissionModelTests: XCTestCase { let permissionManagerMock = PermissionManagerMock() diff --git a/Unit Tests/Permissions/PermissionStoreTests.swift b/Unit Tests/Permissions/PermissionStoreTests.swift index 10f6e2d84e..4566288bf5 100644 --- a/Unit Tests/Permissions/PermissionStoreTests.swift +++ b/Unit Tests/Permissions/PermissionStoreTests.swift @@ -98,7 +98,7 @@ final class PermissionStoreTests: XCTestCase { store.clear(except: [stored1, stored2, stored3]) { [store] error in XCTAssertNil(error) - let permissions = try! store.loadPermissions() // swiftlint:disable:this force_try + let permissions = try! store.loadPermissions() XCTAssertEqual(permissions, [.init(permission: StoredPermission(id: stored1.id, decision: .allow), domain: "duckduckgo.com", diff --git a/Unit Tests/Permissions/WebViewMock.swift b/Unit Tests/Permissions/WebViewMock.swift index e0de0b574e..4aa8b9ab8b 100644 --- a/Unit Tests/Permissions/WebViewMock.swift +++ b/Unit Tests/Permissions/WebViewMock.swift @@ -145,11 +145,11 @@ final class WebViewMock: WKWebView { } @objc final class WKSecurityOriginMock: WKSecurityOrigin { - var _protocol: String! // swiftlint:disable:this identifier_name + var _protocol: String! override var `protocol`: String { _protocol } - var _host: String! // swiftlint:disable:this identifier_name + var _host: String! override var host: String { _host } - var _port: Int! // swiftlint:disable:this identifier_name + var _port: Int! override var port: Int { _port } internal func setURL(_ url: URL) { @@ -167,13 +167,13 @@ final class WebViewMock: WKWebView { } final class WKFrameInfoMock: WKFrameInfo { - var _isMainFrame: Bool! // swiftlint:disable:this identifier_name + var _isMainFrame: Bool! override var isMainFrame: Bool { _isMainFrame } - var _request: URLRequest! // swiftlint:disable:this identifier_name + var _request: URLRequest! override var request: URLRequest { _request } - var _securityOrigin: WKSecurityOrigin! // swiftlint:disable:this identifier_name + var _securityOrigin: WKSecurityOrigin! override var securityOrigin: WKSecurityOrigin { _securityOrigin } - weak var _webView: WKWebView? // swiftlint:disable:this identifier_name + weak var _webView: WKWebView? override var webView: WKWebView? { _webView } init(webView: WKWebView, securityOrigin: WKSecurityOrigin, request: URLRequest, isMainFrame: Bool) { diff --git a/Unit Tests/Preferences/AutofillPreferencesModelTests.swift b/Unit Tests/Preferences/AutofillPreferencesModelTests.swift index fc295099af..ff1bc8787c 100644 --- a/Unit Tests/Preferences/AutofillPreferencesModelTests.swift +++ b/Unit Tests/Preferences/AutofillPreferencesModelTests.swift @@ -30,7 +30,6 @@ final class AutofillPreferencesPersistorMock: AutofillPreferencesPersistor { } final class UserAuthenticatorMock: UserAuthenticating { - // swiftlint:disable:next identifier_name var _authenticateUser: (DeviceAuthenticator.AuthenticationReason) -> DeviceAuthenticationResult = { _ in return .success } func authenticateUser(reason: DeviceAuthenticator.AuthenticationReason, result: @escaping (DeviceAuthenticationResult) -> Void) { diff --git a/Unit Tests/Preferences/DefaultBrowserPreferencesTests.swift b/Unit Tests/Preferences/DefaultBrowserPreferencesTests.swift index 78a773d741..c7e2aae416 100644 --- a/Unit Tests/Preferences/DefaultBrowserPreferencesTests.swift +++ b/Unit Tests/Preferences/DefaultBrowserPreferencesTests.swift @@ -26,10 +26,8 @@ final class DefaultBrowserProviderMock: DefaultBrowserProvider { var bundleIdentifier: String = "com.duckduckgo.DefaultBrowserPreferencesTests" var isDefault: Bool = false - // swiftlint:disable identifier_name var _presentDefaultBrowserPrompt: () throws -> Void = {} var _openSystemPreferences: () -> Void = {} - // swiftlint:enable identifier_name func presentDefaultBrowserPrompt() throws { try _presentDefaultBrowserPrompt() @@ -42,7 +40,6 @@ final class DefaultBrowserProviderMock: DefaultBrowserProvider { final class DefaultBrowserPreferencesTests: XCTestCase { - // swiftlint:disable:next implicitly_unwrapped_optional var provider: DefaultBrowserProviderMock! override func setUpWithError() throws { diff --git a/Unit Tests/Preferences/DownloadsPreferencesTests.swift b/Unit Tests/Preferences/DownloadsPreferencesTests.swift index 615bfad859..5d00503fed 100644 --- a/Unit Tests/Preferences/DownloadsPreferencesTests.swift +++ b/Unit Tests/Preferences/DownloadsPreferencesTests.swift @@ -25,7 +25,6 @@ struct DownloadsPreferencesPersistorMock: DownloadsPreferencesPersistor { var defaultDownloadLocation: URL? var lastUsedCustomDownloadLocation: String? - // swiftlint:disable:next identifier_name var _isDownloadLocationValid: (URL) -> Bool func isDownloadLocationValid(_ location: URL) -> Bool { @@ -56,7 +55,7 @@ class DownloadsPreferencesTests: XCTestCase { deleteTemporaryTestDirectory() } - + func testWhenAlwaysAskIsOnAndCustomLocationWasSetThenEffectiveDownloadLocationIsReturned() { let testDirectory = createTemporaryTestDirectory() let persistor = DownloadsPreferencesPersistorMock(selectedDownloadLocation: nil, alwaysRequestDownloadLocation: true) @@ -66,14 +65,14 @@ class DownloadsPreferencesTests: XCTestCase { XCTAssertEqual(preferences.effectiveDownloadLocation, testDirectory) } - + func testWhenAlwaysAskIsOnAndCustomLocationWasNotSetThenEffectiveDownloadLocationIsReturned() { let persistor = DownloadsPreferencesPersistorMock(selectedDownloadLocation: nil, alwaysRequestDownloadLocation: true) let preferences = DownloadsPreferences(persistor: persistor) XCTAssertEqual(preferences.effectiveDownloadLocation, DownloadsPreferences.defaultDownloadLocation()) } - + func testWhenAlwaysAskIsOnAndCustomLocationWasSetAndRemovedFromDiskThenEffectiveDownloadLocationIsReturned() { let testDirectory = createTemporaryTestDirectory() let persistor = DownloadsPreferencesPersistorMock(selectedDownloadLocation: nil, alwaysRequestDownloadLocation: true) @@ -81,10 +80,10 @@ class DownloadsPreferencesTests: XCTestCase { preferences.lastUsedCustomDownloadLocation = testDirectory deleteTemporaryTestDirectory() - + XCTAssertEqual(preferences.effectiveDownloadLocation, DownloadsPreferences.defaultDownloadLocation()) } - + func testWhenSettingNilDownloadLocationThenDefaultDownloadLocationIsReturned() { let testDirectory = createTemporaryTestDirectory() let persistor = DownloadsPreferencesPersistorMock(selectedDownloadLocation: nil) diff --git a/Unit Tests/Preferences/PreferencesSidebarModelTests.swift b/Unit Tests/Preferences/PreferencesSidebarModelTests.swift index ee4a2de55c..634e040b5d 100644 --- a/Unit Tests/Preferences/PreferencesSidebarModelTests.swift +++ b/Unit Tests/Preferences/PreferencesSidebarModelTests.swift @@ -29,7 +29,6 @@ final class PreferencesSidebarModelTests: XCTestCase { cancellables.removeAll() } - // swiftlint:disable:next identifier_name private func PreferencesSidebarModel(loadSections: [PreferencesSection]? = nil, tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes) -> DuckDuckGo_Privacy_Browser.PreferencesSidebarModel { return DuckDuckGo_Privacy_Browser.PreferencesSidebarModel(loadSections: { loadSections ?? PreferencesSection.defaultSections(includingPrivatePlayer: false) }, tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: MockPrivacyConfigurationManager()) } diff --git a/Unit Tests/Privacy Reference Tests/BrokenSiteReportingReferenceTests.swift b/Unit Tests/Privacy Reference Tests/BrokenSiteReportingReferenceTests.swift index ab4fe02d64..7d0b08b92c 100644 --- a/Unit Tests/Privacy Reference Tests/BrokenSiteReportingReferenceTests.swift +++ b/Unit Tests/Privacy Reference Tests/BrokenSiteReportingReferenceTests.swift @@ -24,11 +24,11 @@ import BrowserServicesKit final class BrokenSiteReportingReferenceTests: XCTestCase { private let testHelper = PrivacyReferenceTestHelper() - + private enum Resource { static let tests = "privacy-reference-tests/broken-site-reporting/tests.json" } - + private func makeURLRequest(with parameters: [String: String]) -> URLRequest { APIRequest.urlRequestFor( url: URL.pixelUrl(forPixelNamed: Pixel.Event.brokenSiteReport.name), @@ -49,11 +49,11 @@ final class BrokenSiteReportingReferenceTests: XCTestCase { os_log("Skipping test, ignore platform for [%s]", type: .info, test.name) continue } - + os_log("Testing [%s]", type: .info, test.name) - + let category = WebsiteBreakage.Category(rawValue: test.category) - + let breakage = WebsiteBreakage(category: category, description: nil, siteUrlString: test.siteURL, @@ -66,31 +66,31 @@ final class BrokenSiteReportingReferenceTests: XCTestCase { ampURL: "", urlParametersRemoved: false, manufacturer: test.manufacturer ?? "") - + let request = makeURLRequest(with: breakage.requestParameters) - + guard let requestURL = request.url else { XCTFail("Couldn't create request URL") return } - + let absoluteURL = requestURL.absoluteString - + if test.expectReportURLPrefix.count > 0 { XCTAssertTrue(requestURL.absoluteString.contains(test.expectReportURLPrefix), "Prefix [\(test.expectReportURLPrefix)] not found") } - + for param in test.expectReportURLParams { let pattern = "[?&]\(param.name)=\(param.value)[&$]?" - + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { XCTFail("Couldn't create regex") return } - + let match = regex.matches(in: absoluteURL, range: NSRange(location: 0, length: absoluteURL.count)) - + XCTAssertEqual(match.count, 1, "Param [\(param.name)] with value [\(param.value)] not found in [\(absoluteURL)]") } } diff --git a/Unit Tests/Privacy Reference Tests/PrivacyReferenceTestHelper.swift b/Unit Tests/Privacy Reference Tests/PrivacyReferenceTestHelper.swift index 2c2e710ee4..4d0dec5e1d 100644 --- a/Unit Tests/Privacy Reference Tests/PrivacyReferenceTestHelper.swift +++ b/Unit Tests/Privacy Reference Tests/PrivacyReferenceTestHelper.swift @@ -26,36 +26,36 @@ struct PrivacyReferenceTestHelper { case unknownFile case invalidFileContents } - + func data(for path: String, in bundle: Bundle) throws -> Data { let url = bundle.resourceURL!.appendingPathComponent(path) let path = url.path return try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) } - + func decodeResource(_ path: String, from bundle: Bundle) -> T { do { let data = try data(for: path, in: bundle) let jsonResult = try JSONDecoder().decode(T.self, from: data) return jsonResult - + } catch { fatalError("Can't decode \(path) - Error \(error.localizedDescription)") } } - + func privacyConfigurationData(withConfigPath path: String, bundle: Bundle) -> PrivacyConfigurationData { guard let configData = try? data(for: path, in: bundle) else { fatalError("Can't decode \(path)") } - + guard let json = try? JSONSerialization.jsonObject(with: configData, options: []) as? [String: Any] else { fatalError("Can't decode \(path)") } - + return PrivacyConfigurationData(json: json) } - + func privacyConfiguration(withData data: PrivacyConfigurationData) -> PrivacyConfiguration { let domain = MockDomainsProtectionStore() return AppPrivacyConfiguration(data: data, identifier: UUID().uuidString, localProtection: domain) diff --git a/Unit Tests/Secure Vault/PasswordManagementItemListModelTests.swift b/Unit Tests/Secure Vault/PasswordManagementItemListModelTests.swift index c6e31b7a07..187c552eb4 100644 --- a/Unit Tests/Secure Vault/PasswordManagementItemListModelTests.swift +++ b/Unit Tests/Secure Vault/PasswordManagementItemListModelTests.swift @@ -41,7 +41,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { model.selectFirst() XCTAssertEqual(model.selected?.id, String(describing: accounts[0])) - + } func testWhenFilterAppliedThenDisplayedAccountsOnlyContainFilteredMatches() { @@ -87,36 +87,36 @@ final class PasswordManagementItemListModelTests: XCTestCase { XCTAssertEqual(model.emptyState, .noData) } - + func testWhenGettingEmptyState_AndViewModelHasData_AndCategoryIsAllItems_AndViewModelIsFiltered_ThenEmptyStateIsNone() { let accounts = (0 ..< 10).map { makeAccount(id: $0, domain: "domain\($0)") } let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) - + model.update(items: accounts) XCTAssertEqual(model.emptyState, .none) - + model.filter = "domain" XCTAssertEqual(model.emptyState, .none) - + model.filter = "filter that won't match" XCTAssertEqual(model.emptyState, .none) } - + func testWhenGettingEmptyState_AndViewModelHasData_AndCategoryIsLogins_AndViewModelIsFiltered_ThenEmptyStateIsLogins() { let accounts = (0 ..< 10).map { makeAccount(id: $0, domain: "domain\($0)") } let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) - + model.update(items: accounts) model.sortDescriptor.category = .logins XCTAssertEqual(model.emptyState, .none) - + model.filter = "domain" XCTAssertEqual(model.emptyState, .none) - + model.filter = "filter that won't match" XCTAssertEqual(model.emptyState, .logins) } - + func makeAccount(id: Int64, title: String? = nil, username: String = "username", domain: String = "domain") -> SecureVaultItem { let account = SecureVaultModels.WebsiteAccount(id: String(id), title: title, diff --git a/Unit Tests/Secure Vault/PasswordManagementListSectionTests.swift b/Unit Tests/Secure Vault/PasswordManagementListSectionTests.swift index 9e040998a5..8979556855 100644 --- a/Unit Tests/Secure Vault/PasswordManagementListSectionTests.swift +++ b/Unit Tests/Secure Vault/PasswordManagementListSectionTests.swift @@ -22,7 +22,7 @@ import XCTest @testable import DuckDuckGo_Privacy_Browser final class PasswordManagementListSectionTests: XCTestCase { - + private lazy var accounts = [ login(named: "Alfa"), login(named: "Alfa Two"), @@ -52,12 +52,12 @@ final class PasswordManagementListSectionTests: XCTestCase { let sections = PasswordManagementListSection.sections(with: [], by: \.id, order: .ascending) XCTAssertTrue(sections.isEmpty) } - + func testWhenSortingItemsByTitle_AndOrderIsAscending_ThenSectionsAreAlphabetical() { let sections = PasswordManagementListSection.sections(with: accounts, by: \.firstCharacter, order: .ascending) XCTAssertEqual(sections.count, 5) XCTAssertEqual(sections.map(\.title), ["A", "B", "C", "Y", "Z"]) - + XCTAssertEqual(sections.first!.items.map(\.title), ["Alfa", "Alfa Two"]) XCTAssertEqual(sections.last!.items.map(\.title), ["Zulu", "Zulu Two"]) } @@ -72,11 +72,11 @@ final class PasswordManagementListSectionTests: XCTestCase { let sections = PasswordManagementListSection.sections(with: accounts, by: \.firstCharacter, order: .descending) XCTAssertEqual(sections.count, 5) XCTAssertEqual(sections.map(\.title), ["Z", "Y", "C", "B", "A"]) - + XCTAssertEqual(sections.first!.items.map(\.title), ["Zulu Two", "Zulu"]) XCTAssertEqual(sections.last!.items.map(\.title), ["Alfa Two", "Alfa"]) } - + func testWhenSortingItemsByTitle_AndTitlesUseDigits_ThenOctothorpeTitleIsUsed() { let accounts = [ login(named: "123"), @@ -85,59 +85,59 @@ final class PasswordManagementListSectionTests: XCTestCase { ] let sections = PasswordManagementListSection.sections(with: accounts, by: \.firstCharacter, order: .ascending) - + XCTAssertEqual(sections.count, 1) XCTAssertEqual(sections.first!.title, "#") } - + func testWhenSortingItemsByDate_AndAllMonthsAndYearsAreTheSame_ThenOneSectionIsReturned() { let months = [1, 1, 1, 1, 1] let accounts = months.map { login(named: "Login", month: $0, year: 2000) } let sections = PasswordManagementListSection.sections(with: accounts, by: \.created, order: .ascending) - + XCTAssertEqual(sections.count, 1) XCTAssertEqual(sections.first!.items.count, months.count) XCTAssertEqual(sections.first!.title, "Jan 2000") } - + func testWhenSortingItemsByDate_AndMonthsAreDifferent_ThenMultipleSectionsAreReturned() { let months = 1...12 let accounts = months.map { login(named: "Login", month: $0) } let sections = PasswordManagementListSection.sections(with: accounts, by: \.created, order: .ascending) - + XCTAssertEqual(sections.count, 12) - + for section in sections { XCTAssertEqual(section.items.count, 1) } } - + func testWhenSortingItemsByDate_AndMonthsAreDifferent_AndThereAreMultipleYears_ThenMultipleSectionsAreReturned() { let months = 1...12 let firstYearAccounts = months.map { login(named: "Login", month: $0, year: 2000) } let secondYearAccounts = months.map { login(named: "Login", month: $0, year: 2001) } let allAccounts = firstYearAccounts + secondYearAccounts let sections = PasswordManagementListSection.sections(with: allAccounts, by: \.created, order: .ascending) - + XCTAssertEqual(sections.count, 24) - + for section in sections { XCTAssertEqual(section.items.count, 1) } } - + private func login(named name: String, month: Int = 1, year: Int = 2000) -> SecureVaultItem { let calendar = Calendar.current let components = DateComponents(calendar: calendar, year: year, month: month, day: 1) let date = calendar.date(from: components) ?? Date() - + let account = SecureVaultModels.WebsiteAccount(id: "1", title: name, username: "Username", domain: "\(name).com", created: date, lastUpdated: date) - + return SecureVaultItem.account(account) } diff --git a/Unit Tests/Statistics/ATB/AtbAndVariantCleanupTests.swift b/Unit Tests/Statistics/ATB/AtbAndVariantCleanupTests.swift index fd0cc60c55..cbd702521f 100644 --- a/Unit Tests/Statistics/ATB/AtbAndVariantCleanupTests.swift +++ b/Unit Tests/Statistics/ATB/AtbAndVariantCleanupTests.swift @@ -67,5 +67,5 @@ class AtbAndVariantCleanupTests: XCTestCase { XCTAssertEqual(Constants.variant, mockStorage.variant) } - + } diff --git a/Unit Tests/Statistics/ATB/Mock/MockVariantManager.swift b/Unit Tests/Statistics/ATB/Mock/MockVariantManager.swift index 74ec779572..b30353018a 100644 --- a/Unit Tests/Statistics/ATB/Mock/MockVariantManager.swift +++ b/Unit Tests/Statistics/ATB/Mock/MockVariantManager.swift @@ -27,7 +27,7 @@ struct MockVariantManager: VariantManager { isSupportedBlock = { _ in return newValue } } } - + var isSupportedBlock: (FeatureName) -> Bool var currentVariant: Variant? @@ -40,7 +40,7 @@ struct MockVariantManager: VariantManager { func assignVariantIfNeeded(_ newInstallCompletion: (VariantManager) -> Void) { } - + func isSupported(feature: FeatureName) -> Bool { return isSupportedBlock(feature) } diff --git a/Unit Tests/Statistics/ATB/StatisticsLoaderTests.swift b/Unit Tests/Statistics/ATB/StatisticsLoaderTests.swift index f6062c381c..d5d8027c6d 100644 --- a/Unit Tests/Statistics/ATB/StatisticsLoaderTests.swift +++ b/Unit Tests/Statistics/ATB/StatisticsLoaderTests.swift @@ -128,20 +128,20 @@ class StatisticsLoaderTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - + func testWhenAppRefreshHasSuccessfulAtbRequestThenAppRetentionAtbUpdated() { - + mockStatisticsStore.atb = "atb" mockStatisticsStore.appRetentionAtb = "retentionatb" loadSuccessfulAtbStub() - + let expect = expectation(description: "Successful atb updates retention store") testee.refreshAppRetentionAtb { XCTAssertEqual(self.mockStatisticsStore.atb, "atb") XCTAssertEqual(self.mockStatisticsStore.appRetentionAtb, "v77-5") expect.fulfill() } - + waitForExpectations(timeout: 1, handler: nil) } @@ -159,19 +159,19 @@ class StatisticsLoaderTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - + func testWhenAppRefreshHasUnsuccessfulAtbRequestThenSearchRetentionAtbNotUpdated() { mockStatisticsStore.atb = "atb" mockStatisticsStore.appRetentionAtb = "retentionAtb" loadUnsuccessfulAtbStub() - + let expect = expectation(description: "Unsuccessful atb does not update store") testee.refreshAppRetentionAtb { XCTAssertEqual(self.mockStatisticsStore.atb, "atb") XCTAssertEqual(self.mockStatisticsStore.appRetentionAtb, "retentionAtb") expect.fulfill() } - + waitForExpectations(timeout: 1, handler: nil) } @@ -278,7 +278,7 @@ class StatisticsLoaderTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } - + func testWhenRefreshRetentionAtbIsPerformedForNonSearchAndNoInstallStatisticsExistThenAtbNotRequested() { loadSuccessfulUpdateAtbStub() diff --git a/Unit Tests/Statistics/ATB/VariantManagerTests.swift b/Unit Tests/Statistics/ATB/VariantManagerTests.swift index 03c3dd2393..4cc5f33255 100644 --- a/Unit Tests/Statistics/ATB/VariantManagerTests.swift +++ b/Unit Tests/Statistics/ATB/VariantManagerTests.swift @@ -30,12 +30,12 @@ class VariantManagerTests: XCTestCase { ] func testWhenVariantIsExcludedThenItIsNotInVariantList() { - + let subject = DefaultVariantManager(variants: testVariants, storage: MockStatisticsStore(), rng: MockVariantRNG(returnValue: 500)) XCTAssertTrue(!subject.isSupported(feature: .dummy)) - + } - + func testWhenCurrentVariantSupportsFeatureThenIsSupportedReturnsTrue() { let testVariants = [ @@ -57,17 +57,17 @@ class VariantManagerTests: XCTestCase { mockStore.atb = "atb" mockStore.appRetentionAtb = "aatb" mockStore.searchRetentionAtb = "satb" - + for i in 0 ..< 100 { - + let subject = DefaultVariantManager(variants: testVariants, storage: mockStore, rng: MockVariantRNG(returnValue: i)) subject.assignVariantIfNeeded { _ in } XCTAssertNotEqual("mt", subject.currentVariant?.name) } - + } - + func testWhenExistingUserThenAssignIfNeededDoesNothing() { let mockStore = MockStatisticsStore() @@ -145,5 +145,5 @@ struct MockVariantRNG: VariantRNG { func nextInt(upperBound: Int) -> Int { return returnValue } - + } diff --git a/Unit Tests/Statistics/CBRCompileTimeReporterTests.swift b/Unit Tests/Statistics/CBRCompileTimeReporterTests.swift index ae77890245..6cb59acc7b 100644 --- a/Unit Tests/Statistics/CBRCompileTimeReporterTests.swift +++ b/Unit Tests/Statistics/CBRCompileTimeReporterTests.swift @@ -239,7 +239,7 @@ class CBRCompileTimeReporterTests: XCTestCase { } } XCTAssertNil(tab1) - + } } diff --git a/Unit Tests/Statistics/LocalStatisticsStoreTests.swift b/Unit Tests/Statistics/LocalStatisticsStoreTests.swift index 65c763f0dc..987e404c9e 100644 --- a/Unit Tests/Statistics/LocalStatisticsStoreTests.swift +++ b/Unit Tests/Statistics/LocalStatisticsStoreTests.swift @@ -25,30 +25,30 @@ class LocalStatisticsStoreTests: XCTestCase { super.setUp() UserDefaultsWrapper.clearAll() } - + override func tearDown() { super.tearDown() UserDefaultsWrapper.clearAll() } - + func testWhenCallingHasInstallStatistics_AndATBExists_ThenItReturnsTrue() { let pixelStore = PixelStoreMock() let store = LocalStatisticsStore(pixelDataStore: pixelStore) store.atb = "atb" - + XCTAssertTrue(store.hasInstallStatistics) XCTAssertTrue(store.hasCurrentOrDeprecatedInstallStatistics) } - + func testWhenCallingHasInstallStatistics_AndLegacyATBExists_ThenItReturnsTrue() { let pixelStore = PixelStoreMock() let store = LocalStatisticsStore(pixelDataStore: pixelStore) pixelStore.set("atb", forKey: "statistics.atb.key") - + XCTAssertFalse(store.hasInstallStatistics) XCTAssertTrue(store.hasCurrentOrDeprecatedInstallStatistics) } - + func testWaitlistUnlocked() { let pixelStore = PixelStoreMock() let store = LocalStatisticsStore(pixelDataStore: pixelStore) @@ -57,50 +57,50 @@ class LocalStatisticsStoreTests: XCTestCase { store.waitlistUnlocked = true XCTAssertTrue(store.waitlistUnlocked) XCTAssertEqual(pixelStore.data.count, 1) - + store.waitlistUnlocked = false XCTAssertFalse(store.waitlistUnlocked) XCTAssertEqual(pixelStore.data.count, 0) } - + // Legacy Statistics: func testWhenInitializingTheLocalStatisticsStore_ThenLegacyStatisticsAreCleared() { var legacyStore = LocalStatisticsStore.LegacyStatisticsStore() legacyStore.atb = "atb" - + XCTAssertNotNil(legacyStore.atb) XCTAssertFalse(legacyStore.legacyStatisticsStoreDataCleared) - + let pixelStore = PixelStoreMock() _ = LocalStatisticsStore(pixelDataStore: pixelStore) - + XCTAssertNil(legacyStore.atb) XCTAssertTrue(legacyStore.legacyStatisticsStoreDataCleared) } - + func testWhenClearingATBData_AndATBDataExists_ThenLegacyStatisticsStoreDataClearedIsTrue() { var legacyStore = LocalStatisticsStore.LegacyStatisticsStore() legacyStore.atb = "atb" legacyStore.clear() - + XCTAssertTrue(legacyStore.legacyStatisticsStoreDataCleared) } - + func testWhenClearingATBData_AndATBDataDoesNotExist_ThenLegacyStatisticsStoreDataClearedIsFalse() { var legacyStore = LocalStatisticsStore.LegacyStatisticsStore() legacyStore.clear() - + XCTAssertFalse(legacyStore.legacyStatisticsStoreDataCleared) } - + func testWhenClearingATBData_AndATBDataExists_AndClearIsCalledMultipleTimes_ThenLegacyStatisticsStoreDataClearedIsTrue() { var legacyStore = LocalStatisticsStore.LegacyStatisticsStore() legacyStore.installDate = Date() legacyStore.clear() legacyStore.clear() legacyStore.clear() - + XCTAssertTrue(legacyStore.legacyStatisticsStoreDataCleared) } diff --git a/Unit Tests/Statistics/PixelEventTests.swift b/Unit Tests/Statistics/PixelEventTests.swift index 77ce128739..dd6ed95a8a 100644 --- a/Unit Tests/Statistics/PixelEventTests.swift +++ b/Unit Tests/Statistics/PixelEventTests.swift @@ -25,7 +25,7 @@ final class PixelEventTests: XCTestCase { func testWhenFormattingJSPixel_ThenJSPixelIncludesPixelName() throws { let pixel = AutofillUserScript.JSPixel(pixelName: "pixel_name") let event = Pixel.Event.jsPixel(pixel) - + XCTAssertEqual(event.name, "m_mac_pixel_name") } diff --git a/Unit Tests/Statistics/PixelTests.swift b/Unit Tests/Statistics/PixelTests.swift index b1a6f2c13b..45c9a48b6f 100644 --- a/Unit Tests/Statistics/PixelTests.swift +++ b/Unit Tests/Statistics/PixelTests.swift @@ -22,7 +22,7 @@ import OHHTTPStubsSwift @testable import DuckDuckGo_Privacy_Browser class PixelTests: XCTestCase { - + let host = "improving.duckduckgo.com" let testAgent = "Test Agent" let userAgentName = "User-Agent" @@ -64,7 +64,7 @@ class PixelTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - + func testWhenPixelFiredThenAPIHeadersAreAdded() { let expectation = XCTestExpectation() diff --git a/Unit Tests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/Unit Tests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index c585ac7806..901bab30db 100644 --- a/Unit Tests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/Unit Tests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -102,17 +102,17 @@ final class SuggestionContainerViewModelTests: XCTestCase { func testSelectPreviousIfPossible() { let suggestionListViewModel = SuggestionContainerViewModel.aSuggestionContainerViewModel - + suggestionListViewModel.selectPreviousIfPossible() XCTAssertEqual(suggestionListViewModel.selectionIndex, suggestionListViewModel.numberOfSuggestions - 1) - + suggestionListViewModel.selectPreviousIfPossible() XCTAssertEqual(suggestionListViewModel.selectionIndex, suggestionListViewModel.numberOfSuggestions - 2) - + let firstIndex = 0 suggestionListViewModel.select(at: firstIndex) XCTAssertEqual(suggestionListViewModel.selectionIndex, firstIndex) - + suggestionListViewModel.selectPreviousIfPossible() XCTAssertNil(suggestionListViewModel.selectionIndex) } diff --git a/Unit Tests/Suggestions/ViewModel/SuggestionViewModelTests.swift b/Unit Tests/Suggestions/ViewModel/SuggestionViewModelTests.swift index 91e91ac6f3..8171c934e4 100644 --- a/Unit Tests/Suggestions/ViewModel/SuggestionViewModelTests.swift +++ b/Unit Tests/Suggestions/ViewModel/SuggestionViewModelTests.swift @@ -26,10 +26,10 @@ final class SuggestionViewModelTests: XCTestCase { let phrase = "phrase" let suggestion = Suggestion.phrase(phrase: phrase) let suggestionViewModel = SuggestionViewModel(suggestion: suggestion, userStringValue: "") - + XCTAssertEqual(phrase, suggestionViewModel.string) } - + func testWhenSuggestionIsWebsite_ThenStringIsUrlStringWithoutSchemeAndWWW() { let urlString = "https://spreadprivacy.com" let url = URL(string: urlString)! diff --git a/Unit Tests/Tab Bar/ViewModel/TabLazyLoaderTests.swift b/Unit Tests/Tab Bar/ViewModel/TabLazyLoaderTests.swift index 608c9217b6..61b3be0fe0 100644 --- a/Unit Tests/Tab Bar/ViewModel/TabLazyLoaderTests.swift +++ b/Unit Tests/Tab Bar/ViewModel/TabLazyLoaderTests.swift @@ -92,7 +92,6 @@ private final class TabLazyLoaderDataSourceMock: TabLazyLoaderDataSource { class TabLazyLoaderTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional private var dataSource: TabLazyLoaderDataSourceMock! var cancellables = Set() diff --git a/Unit Tests/User Agent/Model/UserAgentTests.swift b/Unit Tests/User Agent/Model/UserAgentTests.swift index 51d77fd0cf..174de50991 100644 --- a/Unit Tests/User Agent/Model/UserAgentTests.swift +++ b/Unit Tests/User Agent/Model/UserAgentTests.swift @@ -38,13 +38,13 @@ final class UserAgentTests: XCTestCase { XCTAssert(!UserAgent.for(URL.duckDuckGo).contains("Safari")) XCTAssert(!UserAgent.for(URL.duckDuckGo).contains("Chrome")) } - + func testWhenUserAgentIsDuckDuckGo_ThenUserAgentContainsExpectedParameters() { let appVersion = "app_version" let appID = "app_id" let systemVersion = "system_version" let userAgent = UserAgent.duckDuckGoUserAgent(appVersion: appVersion, appID: appID, systemVersion: systemVersion) - + XCTAssertEqual(userAgent, "ddg_mac/\(appVersion) (\(appID); macOS \(systemVersion))") } From ba77b8474f72cdddfbe2425cdad6143492269243 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 21 Dec 2022 20:57:56 +0600 Subject: [PATCH 12/29] fix RELEASE linter issues --- DuckDuckGo/History/Services/HistoryStore.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/History/Services/HistoryStore.swift b/DuckDuckGo/History/Services/HistoryStore.swift index c56235d43f..0e2dab72ea 100644 --- a/DuckDuckGo/History/Services/HistoryStore.swift +++ b/DuckDuckGo/History/Services/HistoryStore.swift @@ -33,7 +33,7 @@ protocol HistoryStoring { final class HistoryStore: HistoryStoring { init() {} - + init(context: NSManagedObjectContext) { self.context = context } @@ -42,7 +42,7 @@ final class HistoryStore: HistoryStoring { case storeDeallocated case savingFailed } - + private lazy var context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History") func removeEntries(_ entries: [HistoryEntry]) -> Future { From c5f38248cfaab70694517b1c53ce073cb6da78a3 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 21 Dec 2022 08:52:50 -0800 Subject: [PATCH 13/29] Fix the testing steps list in the PR template. (#910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1203591896442256/f Tech Design URL: CC: Description: This PR updates the testing steps section of the template. The reason is this: when you are editing a list in GitHub and hit return, it will automatically increment the list and put you on a new line. Also, when you have a list that starts with 1. on each line, GitHub will calculate its actual offset automatically. However, our list starts with two 1. entries, meaning that when you hit return on the second line you get this: 1. 1. 2. 3. etc. This PR removes the second 1. entry so that the list indices are accurate. Note: GitHub still displays the list correctly when formatted above, so this PR is almost inconsequential – it's just that I keep manually fixing the list when this happens, and I'd rather just fix it for good. 🙂 --- .github/PULL_REQUEST_TEMPLATE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c42924fa19..aac164771b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,7 +18,6 @@ If at any point it isn't actively being worked on/ready for review/otherwise mov **Steps to test this PR**: 1. -1. **Testing checklist**: From 79acb7847eb504013684032e38c431fbac6a7fbf Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 21 Dec 2022 08:56:13 -0800 Subject: [PATCH 14/29] Fix a conditional that stops the bookmarks popover from appearing. (#909) Task/Issue URL: https://app.asana.com/0/1199230911884351/1203591896442254/f Tech Design URL: CC: Description: This PR fixes an issue where the bookmarks popover was not appearing when you bookmark a page from the address bar. --- .../Navigation Bar/View/AddressBarButtonsViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift index 235595846f..1e614e5cb9 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarButtonsViewController.swift @@ -280,7 +280,7 @@ final class AddressBarButtonsViewController: NSViewController { } let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() - if bookmarkPopover.isShown { + if !bookmarkPopover.isShown { bookmarkButton.isHidden = false bookmarkPopover.viewController.bookmark = bookmark bookmarkPopover.show(relativeTo: bookmarkButton.bounds, of: bookmarkButton, preferredEdge: .maxY) From a94da0dec7699085792b129cde752d00780ea027 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 21 Dec 2022 09:47:57 -0800 Subject: [PATCH 15/29] Fix favorite URL editing from the new tab page (#913) Task/Issue URL: https://app.asana.com/0/1177771139624306/1203558178464160/f Tech Design URL: CC: Description: This PR fixes an issue where edits to favorites on the new tab page would not take effect on that page, despite taking effect correctly everywhere else. --- DuckDuckGo/Home Page/View/FavoritesView.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Home Page/View/FavoritesView.swift b/DuckDuckGo/Home Page/View/FavoritesView.swift index 8a7ebb99fd..e99290f9aa 100644 --- a/DuckDuckGo/Home Page/View/FavoritesView.swift +++ b/DuckDuckGo/Home Page/View/FavoritesView.swift @@ -299,9 +299,19 @@ struct Favorite: View { let bookmark: Bookmark + // Maintain separate copies of bookmark metadata required by the view, in order to ensure that SwiftUI re-renders correctly. + private let bookmarkTitle: String + private let bookmarkURL: URL + + init(bookmark: Bookmark) { + self.bookmark = bookmark + self.bookmarkTitle = bookmark.title + self.bookmarkURL = bookmark.url + } + var body: some View { - FavoriteTemplate(title: bookmark.title, domain: bookmark.url.host) + FavoriteTemplate(title: bookmarkTitle, domain: bookmarkURL.host) .link { model.open(bookmark) }.contextMenu(ContextMenu(menuItems: { From 92e37959a27d8bb4aff0a8cb3b692fad61b53be3 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 22 Dec 2022 13:25:00 -0800 Subject: [PATCH 16/29] Fix a possible tab user alert crash (#914) Task/Issue URL: https://app.asana.com/0/1199230911884351/1203597545933056/f Tech Design URL: CC: Description: This PR fixes a race condition that can occur when a user interaction dialog is set to nil while it's still active. --- DuckDuckGo/Browser Tab/Model/Tab.swift | 4 ++-- DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index 533cb7ea55..67be8b2e6f 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -440,8 +440,8 @@ final class Tab: NSObject, Identifiable, ObservableObject { didSet { guard let request = userInteractionDialog?.request else { return } request.addCompletionHandler { [weak self, weak request] _ in - if self?.userInteractionDialog?.request === request { - self?.userInteractionDialog = nil + if let self, let request, self.userInteractionDialog?.request === request { + self.userInteractionDialog = nil } } } diff --git a/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift b/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift index 9e472f5784..058a034128 100644 --- a/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift +++ b/DuckDuckGo/Browser Tab/Model/UserDialogRequest.swift @@ -73,7 +73,11 @@ final class UserDialogRequest: UserDialogRequestProtocol { } deinit { - callback?(.failure(.deinitialized)) + guard let callback else { return } + + DispatchQueue.main.async { + callback(.failure(.deinitialized)) + } } } From d1043c452c14651f11c0f94d122be44754728355 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 9 Jan 2023 16:05:15 +0100 Subject: [PATCH 17/29] Use proper video title and youtube.com URL for sharing from Duck Player (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1203611997317283/f Description: Strip “Duck Player - “ prefix from website title and convert duck:// URL to youtube.com before sharing a Duck Player website. --- DuckDuckGo/Menus/SharingMenu.swift | 5 +++-- DuckDuckGo/Youtube Player/PrivatePlayer.swift | 11 +++++++++++ Unit Tests/Youtube Player/PrivatePlayerTests.swift | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Menus/SharingMenu.swift b/DuckDuckGo/Menus/SharingMenu.swift index f549931492..d3802bf822 100644 --- a/DuckDuckGo/Menus/SharingMenu.swift +++ b/DuckDuckGo/Menus/SharingMenu.swift @@ -73,8 +73,9 @@ final class SharingMenu: NSMenu { return } - service.subject = tabViewModel.title - service.perform(withItems: [url]) + let sharingData = PrivatePlayer.shared.sharingData(for: tabViewModel.title, url: url) ?? (tabViewModel.title, url) + service.subject = sharingData.title + service.perform(withItems: [sharingData.url]) } } diff --git a/DuckDuckGo/Youtube Player/PrivatePlayer.swift b/DuckDuckGo/Youtube Player/PrivatePlayer.swift index 6f68e612a7..7f7b456803 100644 --- a/DuckDuckGo/Youtube Player/PrivatePlayer.swift +++ b/DuckDuckGo/Youtube Player/PrivatePlayer.swift @@ -224,6 +224,17 @@ extension PrivatePlayer { return url.isPrivatePlayer ? PrivatePlayer.commonName : nil } + func sharingData(for title: String, url: URL) -> (title: String, url: URL)? { + guard isAvailable, mode != .disabled, url.isPrivatePlayerScheme, let (videoID, timestamp) = url.youtubeVideoParams else { + return nil + } + + let title = title.dropping(prefix: Self.websiteTitlePrefix) + let sharingURL = URL.youtube(videoID, timestamp: timestamp) + + return (title, sharingURL) + } + func title(for page: HomePage.Models.RecentlyVisitedPageModel) -> String? { guard isAvailable, mode != .disabled else { return nil diff --git a/Unit Tests/Youtube Player/PrivatePlayerTests.swift b/Unit Tests/Youtube Player/PrivatePlayerTests.swift index 6d55118a41..3863aa0098 100644 --- a/Unit Tests/Youtube Player/PrivatePlayerTests.swift +++ b/Unit Tests/Youtube Player/PrivatePlayerTests.swift @@ -133,6 +133,16 @@ final class PrivatePlayerTests: XCTestCase { XCTAssertNil(privatePlayer.tabContent(for: .youtube("12345678", timestamp: "10m"))) } + func testThatSharingDataStripsDuckPlayerPrefixFromTitleAndReturnsYoutubeURL() { + let sharingData = privatePlayer.sharingData(for: "Duck Player - sample video", url: "duck://player/12345678?t=10".url!) + XCTAssertEqual(sharingData?.title, "sample video") + XCTAssertEqual(sharingData?.url, URL.youtube("12345678", timestamp: "10")) + } + + func testThatSharingDataForNonPrivatePlayerURLReturnsNil() { + XCTAssertNil(privatePlayer.sharingData(for: "Wikipedia", url: "https://wikipedia.org".url!)) + } + func testThatTitleForRecentlyVisitedPageIsGeneratedForPrivatePlayerFeedItems() { let feedItem = HomePage.Models.RecentlyVisitedPageModel( actualTitle: "Duck Player - A sample video title", From e96b6e21df94c4b8881afad94efbb72bf697c7fd Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:55:29 +0100 Subject: [PATCH 18/29] Source URL of a detected tracker corrected (#917) Task/Issue URL: https://app.asana.com/0/1177771139624306/1203672182033373/f Description: Internal users reported homepage feed shows detected trackers for duckduckgo.com. The issue was an incorrect URL the native layer assumed when JS message with the tracker arrived. --- DuckDuckGo/Browser Tab/Model/Tab.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index 67be8b2e6f..40bbe2756c 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -1052,7 +1052,7 @@ extension Tab: ContentBlockerRulesUserScriptDelegate { } func contentBlockerRulesUserScript(_ script: ContentBlockerRulesUserScript, detectedTracker tracker: DetectedRequest) { - guard let url = webView.url else { return } + guard let url = URL(string: tracker.pageUrl) else { return } privacyInfo?.trackerInfo.addDetectedTracker(tracker, onPageWithURL: url) historyCoordinating.addDetectedTracker(tracker, onURL: url) From a1f831a2585fb2c0e7f510905018f4e719d21914 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 11 Jan 2023 09:23:08 +0100 Subject: [PATCH 19/29] Use refresh button to stop navigation while in progress (#919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1203589937122242/f Description: Replace “Refresh” button with “Stop” button while page loading is in progress. --- .../Images/Stop.imageset/Contents.json | 15 +++++ .../Images/Stop.imageset/Stop-16.pdf | Bin 0 -> 1430 bytes DuckDuckGo/Common/Localizables/UserText.swift | 1 + .../Main/View/MainWindowController.swift | 2 +- .../View/NavigationBar.storyboard | 8 +-- .../View/NavigationBarViewController.swift | 57 ++++++++++-------- 6 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Stop-16.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Contents.json new file mode 100644 index 0000000000..39daef8a00 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Stop-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Stop-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Stop.imageset/Stop-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..53a45ddda17cb7453d4844719afe5507aa95b39b GIT binary patch literal 1430 zcmZWp-*3|}5PtVxaW9iLq$NHlc5G>y#9D?BAWFyE#6w8EuBc5Q$swzQM7s2U zc9mbhzk$Ww^sDN`Z-I#n2jpN}j@I~}N>FKX14T}mlLo4T7~NhQT~!OjL>uFB1t=3M zbJPkBj9NsgB#J492*LMOHeEDRaW6&ChPpPQsKW0^fMqrT=u`DNujN^`BXScH%!nd@m@TNhc}`YUj^?_T##X#cyW$f zRyaBC7%6kwz4UEeH+={9-)R@=q~HGj@{q2|t&hNGzuA>H%`@DyqAykp%>bROA=BD- z%~Mc1mo zl8G*Gl8Fg+W5E;gMcJ48W;+jkZvBIhP>3Ua{sp5oZ7{*{&;$hw>JiGQPd+Dz=KUq) zBndI|LRk>&IfOZ_Muhsd-0giU=;cg5EJ=>@t~vM%INxn9j-sUNrs;tR2Z7VO-9Jy= ThZnSL`#=YkwBqdS-Iv?{8{RSu literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 551b86244b..744b59b812 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -631,6 +631,7 @@ struct UserText { static let navigateBackTooltip = NSLocalizedString("tooltip.navigation.back", value: "Show the previous page\nHold to show history", comment: "Tooltip for the Back button") static let navigateForwardTooltip = NSLocalizedString("tooltip.navigation.forward", value: "Show the next page\nHold to show history", comment: "Tooltip for the Forward button") static let refreshPageTooltip = NSLocalizedString("tooltip.navigation.refresh", value: "Reload this page", comment: "Tooltip for the Refresh button") + static let stopLoadingTooltip = NSLocalizedString("tooltip.navigation.stop", value: "Stop loading this page", comment: "Tooltip for the Stop Navigation button") static let applicationMenuTooltip = NSLocalizedString("tooltip.application-menu.show", value: "Open application menu", comment: "Tooltip for the Application Menu button") static let privacyDashboardTooltip = NSLocalizedString("tooltip.privacy-dashboard.show", value: "Show the Privacy Dashboard and manage site settings", comment: "Tooltip for the Privacy Dashboard button") diff --git a/DuckDuckGo/Main/View/MainWindowController.swift b/DuckDuckGo/Main/View/MainWindowController.swift index 4991d8e886..b8ac4c96d2 100644 --- a/DuckDuckGo/Main/View/MainWindowController.swift +++ b/DuckDuckGo/Main/View/MainWindowController.swift @@ -259,7 +259,7 @@ fileprivate extension NavigationBarViewController { var controlsForUserPrevention: [NSControl?] { return [goBackButton, goForwardButton, - refreshButton, + refreshOrStopButton, optionsButton, bookmarkListButton, passwordManagementButton, diff --git a/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard b/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard index fddba1467d..e71753a1d3 100644 --- a/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard +++ b/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard @@ -1,8 +1,8 @@ - + - + @@ -99,7 +99,7 @@ - + @@ -307,7 +307,7 @@ - + diff --git a/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift b/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift index c958e1b691..3f955e6270 100644 --- a/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/Navigation Bar/View/NavigationBarViewController.swift @@ -30,7 +30,7 @@ final class NavigationBarViewController: NSViewController { @IBOutlet weak var mouseOverView: MouseOverView! @IBOutlet weak var goBackButton: NSButton! @IBOutlet weak var goForwardButton: NSButton! - @IBOutlet weak var refreshButton: NSButton! + @IBOutlet weak var refreshOrStopButton: NSButton! @IBOutlet weak var optionsButton: NSButton! @IBOutlet weak var bookmarkListButton: MouseOverButton! @IBOutlet weak var passwordManagementButton: MouseOverButton! @@ -77,7 +77,7 @@ final class NavigationBarViewController: NSViewController { private var credentialsToSaveCancellable: AnyCancellable? private var passwordManagerNotificationCancellable: AnyCancellable? private var pinnedViewsNotificationCancellable: AnyCancellable? - private var navigationButtonsCancellables = Set() + private var navigationButtonsCancellable: AnyCancellable? private var downloadsCancellables = Set() required init?(coder: NSCoder) { @@ -115,9 +115,9 @@ final class NavigationBarViewController: NSViewController { optionsButton.toolTip = UserText.applicationMenuTooltip - #if DEBUG || REVIEW +#if DEBUG || REVIEW addDebugNotificationListeners() - #endif +#endif } override func viewWillAppear() { @@ -128,7 +128,7 @@ final class NavigationBarViewController: NSViewController { if view.window?.isPopUpWindow == true { goBackButton.isHidden = true goForwardButton.isHidden = true - refreshButton.isHidden = true + refreshOrStopButton.isHidden = true optionsButton.isHidden = true addressBarTopConstraint.constant = 0 addressBarBottomConstraint.constant = 0 @@ -189,13 +189,17 @@ final class NavigationBarViewController: NSViewController { tabCollectionViewModel.insert(tab, selected: false) } - @IBAction func refreshAction(_ sender: NSButton) { + @IBAction func refreshOrStopAction(_ sender: NSButton) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { os_log("%s: Selected tab view model is nil", type: .error, className) return } - selectedTabViewModel.reload() + if selectedTabViewModel.isLoading { + selectedTabViewModel.tab.stopLoading() + } else { + selectedTabViewModel.reload() + } } @IBAction func optionsButtonAction(_ sender: NSButton) { @@ -305,10 +309,11 @@ final class NavigationBarViewController: NSViewController { else { return } DispatchQueue.main.async { [weak self] in guard let self = self, - self.tabCollectionViewModel.selectedTabViewModel?.tab.url == topUrl else { - // if the tab is not active, don't show the popup - return - } + self.tabCollectionViewModel.selectedTabViewModel?.tab.url == topUrl + else { + // if the tab is not active, don't show the popup + return + } self.addressBarViewController?.addressBarButtonsViewController?.showBadgeNotification(.cookieManaged) } } @@ -334,7 +339,7 @@ final class NavigationBarViewController: NSViewController { goBackButton.toolTip = UserText.navigateBackTooltip goForwardButton.toolTip = UserText.navigateForwardTooltip - refreshButton.toolTip = UserText.refreshPageTooltip + refreshOrStopButton.toolTip = UserText.refreshPageTooltip } private func subscribeToSelectedTabViewModel() { @@ -566,25 +571,26 @@ final class NavigationBarViewController: NSViewController { } private func subscribeToNavigationActionFlags() { - navigationButtonsCancellables.forEach { $0.cancel() } - navigationButtonsCancellables.removeAll() + navigationButtonsCancellable = nil guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { goBackButton.isEnabled = false goForwardButton.isEnabled = false + refreshOrStopButton.isEnabled = false return } - selectedTabViewModel.$canGoBack.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.updateNavigationButtons() - } .store(in: &navigationButtonsCancellables) - - selectedTabViewModel.$canGoForward.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.updateNavigationButtons() - } .store(in: &navigationButtonsCancellables) - selectedTabViewModel.$canReload.receive(on: DispatchQueue.main).sink { [weak self] _ in + navigationButtonsCancellable = Publishers.MergeMany( + selectedTabViewModel.$canGoBack.removeDuplicates(), + selectedTabViewModel.$canGoForward.removeDuplicates(), + selectedTabViewModel.$canReload.removeDuplicates(), + selectedTabViewModel.$isLoading.removeDuplicates() + ) + .asVoid() + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.updateNavigationButtons() - } .store(in: &navigationButtonsCancellables) + } } private func updateNavigationButtons() { @@ -595,9 +601,10 @@ final class NavigationBarViewController: NSViewController { goBackButton.isEnabled = selectedTabViewModel.canGoBack goForwardButton.isEnabled = selectedTabViewModel.canGoForward - refreshButton.isEnabled = selectedTabViewModel.canReload + refreshOrStopButton.isEnabled = selectedTabViewModel.canReload || selectedTabViewModel.isLoading + refreshOrStopButton.image = selectedTabViewModel.isLoading ? NSImage(imageLiteralResourceName: "Stop") : NSImage(imageLiteralResourceName: "Refresh") + refreshOrStopButton.toolTip = selectedTabViewModel.isLoading ? UserText.stopLoadingTooltip : UserText.refreshPageTooltip } - } extension NavigationBarViewController: MouseOverViewDelegate { From b2c8c6dca4b7ed92b9579fda36dc04b98101286c Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 11 Jan 2023 09:24:00 +0100 Subject: [PATCH 20/29] Store Xcode project configuration in xcconfig files (#916) Task/Issue URL: https://app.asana.com/0/0/1203649063481852/f (see parent task for more info) Description: This change extracts configuration of all Xcode project targets into a set of xcconfig files. No configuration is defined inside project.pbxproj anymore. The default configuration (defined on the project level, not target level) is kept in Global.xcconfig. Target configurations are defined in respective xcconfig files within the directory tree. --- .github/PULL_REQUEST_TEMPLATE.md | 1 + Configuration/App/AppTargetsBase.xcconfig | 45 ++ Configuration/App/DuckDuckGo.xcconfig | 36 ++ Configuration/Common.xcconfig | 36 ++ Configuration/Global.xcconfig | 87 +++ .../IntegrationTests.xcconfig} | 8 +- Configuration/Tests/TestsTargetsBase.xcconfig | 32 + Configuration/Tests/UnitTests.xcconfig | 23 + Configuration/UITests/UITests.xcconfig | 31 + Configuration/Version.xcconfig | 1 - DuckDuckGo.xcodeproj/project.pbxproj | 593 +++--------------- 11 files changed, 368 insertions(+), 525 deletions(-) create mode 100644 Configuration/App/AppTargetsBase.xcconfig create mode 100644 Configuration/App/DuckDuckGo.xcconfig create mode 100644 Configuration/Common.xcconfig create mode 100644 Configuration/Global.xcconfig rename Configuration/{Configuration.xcconfig => Tests/IntegrationTests.xcconfig} (77%) create mode 100644 Configuration/Tests/TestsTargetsBase.xcconfig create mode 100644 Configuration/Tests/UnitTests.xcconfig create mode 100644 Configuration/UITests/UITests.xcconfig diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aac164771b..dfc7a59b1d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -24,6 +24,7 @@ If at any point it isn't actively being worked on/ready for review/otherwise mov * [ ] Test with Release configuration * [ ] Test proper deallocation of tabs * [ ] Make sure committed submodule changes are desired +* [ ] Make sure configuration is changed only in xcconfig files, not in the Xcode project file directly --- ###### Internal references: diff --git a/Configuration/App/AppTargetsBase.xcconfig b/Configuration/App/AppTargetsBase.xcconfig new file mode 100644 index 0000000000..b51f77f7b2 --- /dev/null +++ b/Configuration/App/AppTargetsBase.xcconfig @@ -0,0 +1,45 @@ +// 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. +// + +#include "../Common.xcconfig" + +ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon +ASSETCATALOG_COMPILER_APPICON_NAME[config=Debug] = Icon - Debug +ASSETCATALOG_COMPILER_APPICON_NAME[config=CI] = Icon - Debug +ASSETCATALOG_COMPILER_APPICON_NAME[config=Review] = Icon - Review + +ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = GlobalAccentColor + +CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES +CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES + +CURRENT_PROJECT_VERSION = $(MARKETING_VERSION) + +ENABLE_HARDENED_RUNTIME = YES + +PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX) +PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug +PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).debug +PRODUCT_BUNDLE_IDENTIFIER[config=Review][sdk=*] = $(BUNDLE_IDENTIFIER_PREFIX).review + +INFOPLIST_FILE = DuckDuckGo/Info.plist + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks + +PRODUCT_MODULE_NAME = $(TARGET_NAME:c99extidentifier) +PRODUCT_NAME = $(PRODUCT_NAME_PREFIX) +PRODUCT_NAME[config=Review] = $(PRODUCT_NAME_PREFIX) Review + +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/DuckDuckGo/Bridging.h diff --git a/Configuration/App/DuckDuckGo.xcconfig b/Configuration/App/DuckDuckGo.xcconfig new file mode 100644 index 0000000000..de8b3e0037 --- /dev/null +++ b/Configuration/App/DuckDuckGo.xcconfig @@ -0,0 +1,36 @@ +// 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. +// + +#include "AppTargetsBase.xcconfig" + +BUNDLE_IDENTIFIER_PREFIX = com.duckduckgo.macos.browser + +CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements +CODE_SIGN_ENTITLEMENTS[config=CI] = DuckDuckGo/DuckDuckGoCI.entitlements + +CODE_SIGN_STYLE[sdk=*] = Manual +CODE_SIGN_STYLE[config=Debug][sdk=*] = Automatic + +CODE_SIGN_IDENTITY[sdk=macosx*] = Developer ID Application +CODE_SIGN_IDENTITY[config=Debug][sdk=macosx*] = Apple Development +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +PRODUCT_NAME_PREFIX = DuckDuckGo + +PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = +PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = MacOS Browser +PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = MacOS Browser Product Review + +#include? "../../LocalOverrides.xcconfig" diff --git a/Configuration/Common.xcconfig b/Configuration/Common.xcconfig new file mode 100644 index 0000000000..84c5c3f8da --- /dev/null +++ b/Configuration/Common.xcconfig @@ -0,0 +1,36 @@ +// 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. +// + +#include "Version.xcconfig" + +COMBINE_HIDPI_IMAGES = YES + +DEVELOPMENT_TEAM = HKE973VLUW +DEVELOPMENT_TEAM[config=CI][sdk=*] = + +FEATURE_FLAGS = FEEDBACK + +GCC_PREPROCESSOR_DEFINITIONS[config=CI][arch=*][sdk=*] = DEBUG=1 CI=1 $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[config=Debug][arch=*][sdk=*] = DEBUG=1 $(inherited) +GCC_PREPROCESSOR_DEFINITIONS[config=Review][arch=*][sdk=*] = REVIEW=1 $(inherited) + +MACOSX_DEPLOYMENT_TARGET = 10.15 + +SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*][sdk=*] = $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=CI][arch=*][sdk=*] = DEBUG CI $(FEATURE_FLAGS) +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug][arch=*][sdk=*] = DEBUG $(FEATURE_FLAGS) + +SWIFT_VERSION = 5.0 + diff --git a/Configuration/Global.xcconfig b/Configuration/Global.xcconfig new file mode 100644 index 0000000000..f7684eef2f --- /dev/null +++ b/Configuration/Global.xcconfig @@ -0,0 +1,87 @@ +// 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. +// + +ALWAYS_SEARCH_USER_PATHS = NO +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE +CLANG_CXX_LANGUAGE_STANDARD = gnu++14 +CLANG_CXX_LIBRARY = libc++ +CLANG_ENABLE_MODULES = YES +CLANG_ENABLE_OBJC_ARC = YES +CLANG_ENABLE_OBJC_WEAK = YES +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_OBJC_LITERAL_CONVERSION = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +COPY_PHASE_STRIP = NO + +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +DEBUG_INFORMATION_FORMAT[config=CI][arch=*][sdk=*] = dwarf +DEBUG_INFORMATION_FORMAT[config=Debug][arch=*][sdk=*] = dwarf + +ENABLE_NS_ASSERTIONS = NO +ENABLE_NS_ASSERTIONS[config=CI][arch=*][sdk=*] = YES +ENABLE_NS_ASSERTIONS[config=Debug][arch=*][sdk=*] = YES + +ENABLE_STRICT_OBJC_MSGSEND = YES +ENABLE_TESTABILITY = YES +GCC_C_LANGUAGE_STANDARD = gnu11 +GCC_DYNAMIC_NO_PIC = NO +GCC_NO_COMMON_BLOCKS = YES +GCC_OPTIMIZATION_LEVEL = 0 +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_VARIABLE = YES + +MTL_ENABLE_DEBUG_INFO = NO +MTL_ENABLE_DEBUG_INFO[config=CI][arch=*][sdk=*] = INCLUDE_SOURCE +MTL_ENABLE_DEBUG_INFO[config=Debug][arch=*][sdk=*] = INCLUDE_SOURCE + +MTL_FAST_MATH = YES + +ONLY_ACTIVE_ARCH = NO +ONLY_ACTIVE_ARCH[config=Debug][arch=*][sdk=*] = YES +ONLY_ACTIVE_ARCH[config=CI][arch=*][sdk=*] = YES + +SDKROOT = macosx + +SWIFT_OPTIMIZATION_LEVEL = -O +SWIFT_OPTIMIZATION_LEVEL[config=CI][arch=*][sdk=*] = -Onone +SWIFT_OPTIMIZATION_LEVEL[config=Debug][arch=*][sdk=*] = -Onone + +SWIFT_COMPILATION_MODE = wholemodule +SWIFT_COMPILATION_MODE[config=CI][arch=*][sdk=*] = +SWIFT_COMPILATION_MODE[config=Debug][arch=*][sdk=*] = diff --git a/Configuration/Configuration.xcconfig b/Configuration/Tests/IntegrationTests.xcconfig similarity index 77% rename from Configuration/Configuration.xcconfig rename to Configuration/Tests/IntegrationTests.xcconfig index 2099570d88..6013c7ab6d 100644 --- a/Configuration/Configuration.xcconfig +++ b/Configuration/Tests/IntegrationTests.xcconfig @@ -13,4 +13,10 @@ // limitations under the License. // -#include? "Version.xcconfig" +#include "TestsTargetsBase.xcconfig" + +MACOSX_DEPLOYMENT_TARGET = 11.1 + +INFOPLIST_FILE = Integration Tests/Info.plist +PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.Integration-Tests + diff --git a/Configuration/Tests/TestsTargetsBase.xcconfig b/Configuration/Tests/TestsTargetsBase.xcconfig new file mode 100644 index 0000000000..969c38147d --- /dev/null +++ b/Configuration/Tests/TestsTargetsBase.xcconfig @@ -0,0 +1,32 @@ +// 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. +// + +#include "../Common.xcconfig" + +BUNDLE_LOADER=$(TEST_HOST) + +CODE_SIGN_STYLE = Automatic +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +DEAD_CODE_STRIPPING = YES + +INFOPLIST_FILE = DuckDuckGo/Info.plist + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks + +PRODUCT_MODULE_NAME = $(TARGET_NAME:c99extidentifier) +PRODUCT_NAME = $(TARGET_NAME) + +TEST_HOST=$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo diff --git a/Configuration/Tests/UnitTests.xcconfig b/Configuration/Tests/UnitTests.xcconfig new file mode 100644 index 0000000000..32802fe2f4 --- /dev/null +++ b/Configuration/Tests/UnitTests.xcconfig @@ -0,0 +1,23 @@ +// 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. +// + +#include "TestsTargetsBase.xcconfig" + +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES + +INFOPLIST_FILE = Unit Tests/Info.plist +PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.DuckDuckGoTests + +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/Unit Tests/Common/TestsBridging.h diff --git a/Configuration/UITests/UITests.xcconfig b/Configuration/UITests/UITests.xcconfig new file mode 100644 index 0000000000..dc001b2c10 --- /dev/null +++ b/Configuration/UITests/UITests.xcconfig @@ -0,0 +1,31 @@ +// 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. +// + +#include "../Common.xcconfig" + +CODE_SIGN_STYLE = Automatic +CODE_SIGN_IDENTITY[config=CI][sdk=macosx*] = + +DEAD_CODE_STRIPPING = YES + +MACOSX_DEPLOYMENT_TARGET = 11.3 + +INFOPLIST_FILE = UI Tests/Info.plist +PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.UI-Tests +PRODUCT_NAME = $(TARGET_NAME) + +TEST_TARGET_NAME = DuckDuckGo + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index 44fb222ef5..fed02c2293 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1,2 +1 @@ MARKETING_VERSION = 0.31.4 - diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6be5245d30..d4be0f1cc7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -421,7 +421,6 @@ 85AC7AD927BD625000FFB69B /* HomePageAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85AC7AD827BD625000FFB69B /* HomePageAssets.xcassets */; }; 85AC7ADB27BD628400FFB69B /* HomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC7ADA27BD628400FFB69B /* HomePage.swift */; }; 85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AC7ADC27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift */; }; - 85AE2FF224A33A2D002D507F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85AE2FF124A33A2D002D507F /* WebKit.framework */; }; 85B7184A27677C2D00B4277F /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85B7184927677C2D00B4277F /* Onboarding.storyboard */; }; 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184B27677C6500B4277F /* OnboardingViewController.swift */; }; 85B7184E27677CBB00B4277F /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184D27677CBB00B4277F /* RootView.swift */; }; @@ -965,6 +964,7 @@ 373A1AA9283ED86C00586521 /* BookmarksHTMLReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksHTMLReaderTests.swift; sourceTree = ""; }; 373A1AAF2842C4EA00586521 /* BookmarkHTMLImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkHTMLImporter.swift; sourceTree = ""; }; 373A1AB128451ED400586521 /* BookmarksHTMLImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksHTMLImporterTests.swift; sourceTree = ""; }; + 373A26962964CF0B0043FC57 /* TestsTargetsBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TestsTargetsBase.xcconfig; sourceTree = ""; }; 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModelTests+WithoutPinnedTabsManager.swift"; sourceTree = ""; }; 37534C9D28104D9B002621E7 /* TabLazyLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLazyLoaderTests.swift; sourceTree = ""; }; 37534C9F28113101002621E7 /* LazyLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyLoadable.swift; sourceTree = ""; }; @@ -976,6 +976,7 @@ 3767190128E724B2003A2A15 /* PrivatePlayerURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivatePlayerURLExtension.swift; sourceTree = ""; }; 3767FC6E29227B5900D28741 /* Bookmark 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Bookmark 3.xcdatamodel"; sourceTree = ""; }; 376C4DB828A1A48A00CC0F5B /* FirePopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewModelTests.swift; sourceTree = ""; }; + 37717E66296B5A20002FAEDF /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = ""; }; 3776582C27F71652009A6B35 /* WebsiteBreakageReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteBreakageReportTests.swift; sourceTree = ""; }; 3776582E27F82E62009A6B35 /* AutofillPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillPreferences.swift; sourceTree = ""; }; 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesTests.swift; sourceTree = ""; }; @@ -983,6 +984,13 @@ 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupPreferencesTests.swift; sourceTree = ""; }; 378205FA283C277800D1D4AA /* MainMenuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTests.swift; sourceTree = ""; }; 3783F92229432E1800BCA897 /* WebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTests.swift; sourceTree = ""; }; + 378B5887295CF2A4002C0CC0 /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; + 378B5888295CF2A4002C0CC0 /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; + 378B588B295CF3B9002C0CC0 /* AppTargetsBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppTargetsBase.xcconfig; sourceTree = ""; }; + 378B588C295CF446002C0CC0 /* UITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UITests.xcconfig; sourceTree = ""; }; + 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UnitTests.xcconfig; sourceTree = ""; }; + 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IntegrationTests.xcconfig; sourceTree = ""; }; + 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGo.xcconfig; sourceTree = ""; }; 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesAutofillView.swift; sourceTree = ""; }; 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartupPreferences.swift; sourceTree = ""; }; 37A803DA27FD69D300052F4C /* Data Import Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Data Import Resources"; sourceTree = ""; }; @@ -1311,7 +1319,6 @@ 85B7184927677C2D00B4277F /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; 85B7184B27677C6500B4277F /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 85B7184D27677CBB00B4277F /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; - 85B8757E28B903D900D39E04 /* Configuration.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; 85C48CCB278D808F00D3263E /* NSAttributedStringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedStringExtension.swift; sourceTree = ""; }; 85C48CD027908C1000D3263E /* BrowserImportMoreInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserImportMoreInfoViewController.swift; sourceTree = ""; }; 85C5991A27D10CF000E605B2 /* FireAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireAnimationView.swift; sourceTree = ""; }; @@ -1774,7 +1781,6 @@ buildActionMask = 2147483647; files = ( 9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */, - 85AE2FF224A33A2D002D507F /* WebKit.framework in Frameworks */, 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */, 1E950E432912A10D0051A99B /* UserScript in Frameworks */, 4B82E9B325B69E3E00656FE7 /* TrackerRadarKit in Frameworks */, @@ -2047,6 +2053,46 @@ path = Menus; sourceTree = ""; }; + 378B5886295CF2A4002C0CC0 /* Configuration */ = { + isa = PBXGroup; + children = ( + 37717E66296B5A20002FAEDF /* Global.xcconfig */, + 378B5887295CF2A4002C0CC0 /* Version.xcconfig */, + 378B5888295CF2A4002C0CC0 /* Common.xcconfig */, + 378C76D8296842FD0092E949 /* App */, + 378C76D92968433B0092E949 /* Tests */, + 378C76DA296843460092E949 /* UITests */, + ); + path = Configuration; + sourceTree = ""; + }; + 378C76D8296842FD0092E949 /* App */ = { + isa = PBXGroup; + children = ( + 378B588B295CF3B9002C0CC0 /* AppTargetsBase.xcconfig */, + 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */, + ); + path = App; + sourceTree = ""; + }; + 378C76D92968433B0092E949 /* Tests */ = { + isa = PBXGroup; + children = ( + 373A26962964CF0B0043FC57 /* TestsTargetsBase.xcconfig */, + 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */, + 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */, + ); + path = Tests; + sourceTree = ""; + }; + 378C76DA296843460092E949 /* UITests */ = { + isa = PBXGroup; + children = ( + 378B588C295CF446002C0CC0 /* UITests.xcconfig */, + ); + path = UITests; + sourceTree = ""; + }; 37BF3F12286D8A4B00BD9014 /* Pinned Tabs */ = { isa = PBXGroup; children = ( @@ -3080,7 +3126,7 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( - 85B8757E28B903D900D39E04 /* Configuration.xcconfig */, + 378B5886295CF2A4002C0CC0 /* Configuration */, AA68C3D62490F821001B8783 /* README.md */, AA585D80248FD31100E9A3E2 /* DuckDuckGo */, AA585D93248FD31400E9A3E2 /* Unit Tests */, @@ -5595,640 +5641,141 @@ /* Begin XCBuildConfiguration section */ 4B1AD8A425FC27E200261379 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Integration Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.Integration-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = Debug; }; 4B1AD8A525FC27E200261379 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Integration Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.Integration-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = Release; }; 4B1AD8B025FC322600261379 /* CI */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 85B8757E28B903D900D39E04 /* Configuration.xcconfig */; + baseConfigurationReference = 37717E66296B5A20002FAEDF /* Global.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "CI=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CI"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = CI; }; 4B1AD8B125FC322600261379 /* CI */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "Icon - Debug"; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = GlobalAccentColor; - CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoCI.entitlements; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(MARKETING_VERSION)"; - DEVELOPMENT_TEAM = ""; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = DuckDuckGo/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; - PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; - PRODUCT_NAME = DuckDuckGo; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "FEEDBACK $(inherited)"; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/DuckDuckGo/Bridging.h"; - SWIFT_VERSION = 5.0; }; name = CI; }; 4B1AD8B225FC322600261379 /* CI */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "Unit Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.DuckDuckGoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Unit Tests/Common/TestsBridging.h"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = CI; }; 4B1AD8B325FC322600261379 /* CI */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Integration Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.Integration-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = CI; }; 7B4CE8E126F02108009134B1 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588C295CF446002C0CC0 /* UITests.xcconfig */; buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "UI Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.3; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.UI-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = "DuckDuckGo Privacy Browser"; }; name = Debug; }; 7B4CE8E226F02108009134B1 /* CI */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588C295CF446002C0CC0 /* UITests.xcconfig */; buildSettings = { - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "UI Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.3; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.UI-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = "DuckDuckGo Privacy Browser"; }; name = CI; }; 7B4CE8E326F02108009134B1 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588C295CF446002C0CC0 /* UITests.xcconfig */; buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "UI Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.3; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.UI-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = "DuckDuckGo Privacy Browser"; }; name = Release; }; AA585DA2248FD31500E9A3E2 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 85B8757E28B903D900D39E04 /* Configuration.xcconfig */; + baseConfigurationReference = 37717E66296B5A20002FAEDF /* Global.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; AA585DA3248FD31500E9A3E2 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 85B8757E28B903D900D39E04 /* Configuration.xcconfig */; + baseConfigurationReference = 37717E66296B5A20002FAEDF /* Global.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; AA585DA5248FD31500E9A3E2 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "Icon - Debug"; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = GlobalAccentColor; - CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(MARKETING_VERSION)"; - DEVELOPMENT_TEAM = HKE973VLUW; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = DuckDuckGo/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; - PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; - PRODUCT_NAME = DuckDuckGo; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "FEEDBACK $(inherited)"; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/DuckDuckGo/Bridging.h"; - SWIFT_VERSION = 5.0; }; name = Debug; }; AA585DA6248FD31500E9A3E2 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = GlobalAccentColor; - CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(MARKETING_VERSION)"; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = HKE973VLUW; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = DuckDuckGo/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; - PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; - PRODUCT_NAME = DuckDuckGo; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "MacOS Browser"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = FEEDBACK; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/DuckDuckGo/Bridging.h"; - SWIFT_VERSION = 5.0; }; name = Release; }; AA585DA8248FD31500E9A3E2 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Unit Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.DuckDuckGoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Unit Tests/Common/TestsBridging.h"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = Debug; }; AA585DA9248FD31500E9A3E2 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Unit Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.DuckDuckGoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Unit Tests/Common/TestsBridging.h"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = Release; }; AAE814AB2716DFE8009D3531 /* Review */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 85B8757E28B903D900D39E04 /* Configuration.xcconfig */; + baseConfigurationReference = 37717E66296B5A20002FAEDF /* Global.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Review; }; AAE814AC2716DFE8009D3531 /* Review */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "Icon - Review"; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = GlobalAccentColor; - CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(MARKETING_VERSION)"; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = HKE973VLUW; - ENABLE_HARDENED_RUNTIME = YES; - GCC_PREPROCESSOR_DEFINITIONS = "REVIEW=1"; - INFOPLIST_FILE = DuckDuckGo/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.review; - PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; - PRODUCT_NAME = "DuckDuckGo Review"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "MacOS Browser Product Review"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "FEEDBACK REVIEW"; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/DuckDuckGo/Bridging.h"; - SWIFT_VERSION = 5.0; }; name = Review; }; AAE814AD2716DFE8009D3531 /* Review */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Unit Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.15; - PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.DuckDuckGoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Unit Tests/Common/TestsBridging.h"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = Review; }; AAE814AE2716DFE8009D3531 /* Review */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "Integration Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.1; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.Integration-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DuckDuckGo.app/Contents/MacOS/DuckDuckGo"; }; name = Review; }; AAE814AF2716DFE8009D3531 /* Review */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 378B588C295CF446002C0CC0 /* UITests.xcconfig */; buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = HKE973VLUW; - INFOPLIST_FILE = "UI Tests/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 11.3; - PRODUCT_BUNDLE_IDENTIFIER = "com.duckduckgo.UI-Tests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = "DuckDuckGo Privacy Browser"; }; name = Review; }; From 28a3d170025fdd69673b2d515b62acfba2167bae Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 11 Jan 2023 10:16:31 +0000 Subject: [PATCH 21/29] Fire button privacy reference tests (#899) Task/Issue URL: https://app.asana.com/0/0/1203552409531122/f Description: Add fire button storage privacy reference tests --- DuckDuckGo.xcodeproj/project.pbxproj | 4 + .../FireproofingReferenceTests.swift | 137 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 Unit Tests/Privacy Reference Tests/FireproofingReferenceTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d4be0f1cc7..83cb88fc2f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 1E950E412912A10D0051A99B /* PrivacyDashboard in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E402912A10D0051A99B /* PrivacyDashboard */; }; 1E950E432912A10D0051A99B /* UserScript in Frameworks */ = {isa = PBXBuildFile; productRef = 1E950E422912A10D0051A99B /* UserScript */; }; 3106AD76287F000600159FE5 /* CookieConsentUserPermissionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */; }; + 310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */; }; 311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B262628E73E0A00FD181A /* TabShadowConfig.swift */; }; 313AEDA1287CAD1D00E1E8F4 /* CookieConsentUserPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313AEDA0287CAD1D00E1E8F4 /* CookieConsentUserPermissionView.swift */; }; 3154FD1428E6011A00909769 /* TabShadowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3154FD1328E6011A00909769 /* TabShadowView.swift */; }; @@ -925,6 +926,7 @@ 1E7E2E9129029F9B00C01B54 /* WebsiteBreakageReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteBreakageReporter.swift; sourceTree = ""; }; 1E7E2E932902AC0E00C01B54 /* PrivacyDashboardPermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPermissionHandler.swift; sourceTree = ""; }; 3106AD75287F000600159FE5 /* CookieConsentUserPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentUserPermissionViewController.swift; sourceTree = ""; }; + 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireproofingReferenceTests.swift; sourceTree = ""; }; 311B262628E73E0A00FD181A /* TabShadowConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowConfig.swift; sourceTree = ""; }; 313AEDA0287CAD1D00E1E8F4 /* CookieConsentUserPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieConsentUserPermissionView.swift; sourceTree = ""; }; 3154FD1328E6011A00909769 /* TabShadowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabShadowView.swift; sourceTree = ""; }; @@ -1945,6 +1947,7 @@ children = ( 31E163BE293A580000963C10 /* Resources */, 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */, + 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */, 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */, ); path = "Privacy Reference Tests"; @@ -5491,6 +5494,7 @@ AA63745424C9BF9A00AB2AC4 /* SuggestionContainerTests.swift in Sources */, AAC9C01524CAFBCE00AD1325 /* TabTests.swift in Sources */, B69B504C2726CA2900758A2B /* MockVariantManager.swift in Sources */, + 310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */, B610F2EC27AA8F9400FCEBE9 /* ContentBlockerRulesManagerMock.swift in Sources */, B6BBF1722744CE36004F850E /* FireproofDomainsStoreMock.swift in Sources */, 4BA1A6D9258C0CB300F6F690 /* DataEncryptionTests.swift in Sources */, diff --git a/Unit Tests/Privacy Reference Tests/FireproofingReferenceTests.swift b/Unit Tests/Privacy Reference Tests/FireproofingReferenceTests.swift new file mode 100644 index 0000000000..4cb698c7ee --- /dev/null +++ b/Unit Tests/Privacy Reference Tests/FireproofingReferenceTests.swift @@ -0,0 +1,137 @@ +// +// FireproofingReferenceTests.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 XCTest +import os.log +@testable import DuckDuckGo_Privacy_Browser + +final class FireproofingReferenceTests: XCTestCase { + private var referenceTests = [Test]() + private let dataStore = WKWebsiteDataStore.default() + private let fireproofDomains = FireproofDomains.shared + + private enum Resource { + static let tests = "privacy-reference-tests/storage-clearing/tests.json" + } + + private lazy var testData: TestData = { + let bundle = Bundle(for: BrokenSiteReportingReferenceTests.self) + let testData: TestData = PrivacyReferenceTestHelper().decodeResource(Resource.tests, from: bundle) + return testData + }() + + private func sanitizedSite(_ site: String) -> String { + let url = URL(string: site)! + return url.host! + } + + override func tearDownWithError() throws { + referenceTests.removeAll() + } + + func testFireproofing() throws { + referenceTests = testData.fireButtonFireproofing.tests.filter { + $0.exceptPlatforms.contains("macos-browser") == false + } + + let testsExecuted = expectation(description: "tests executed") + testsExecuted.expectedFulfillmentCount = referenceTests.count + + runReferenceTests(onTestExecuted: testsExecuted) + + waitForExpectations(timeout: 30, handler: nil) + } + + private func runReferenceTests(onTestExecuted: XCTestExpectation) { + + guard let test = referenceTests.popLast() else { + return + } + + os_log("Testing %s", test.name) + + let loginDomains = testData.fireButtonFireproofing.fireproofedSites.map { sanitizedSite($0) } + let logins = MockPreservedLogins(domains: loginDomains) + + let webCacheManager = WebCacheManager(fireproofDomains: logins, websiteDataStore: dataStore) + + guard let cookie = cookie(for: test) else { + XCTFail("Cookie should exist for test \(test.name)") + return + } + + Task { @MainActor () -> Void in + await dataStore.cookieStore?.setCookie(cookie) + await webCacheManager.clear() + + let hotCookies = await dataStore.cookieStore?.allCookies() + let testCookie = hotCookies?.filter { $0.name == test.cookieName }.first + + if test.expectCookieRemoved { + XCTAssertNil(testCookie, "Cookie should not exist for test: \(test.name)") + } else { + XCTAssertNotNil(testCookie, "Cookie should exist for test: \(test.name)") + } + + if let cookie = testCookie { + await dataStore.cookieStore?.deleteCookie(cookie) + } + DispatchQueue.main.async { + onTestExecuted.fulfill() + self.runReferenceTests(onTestExecuted: onTestExecuted) + } + } + } + + private func cookie(for test: Test) -> HTTPCookie? { + HTTPCookie(properties: [.name: test.cookieName, + .path: "", + .domain: test.cookieDomain, + .value: "123"]) + } + + private class MockPreservedLogins: FireproofDomains { + + init(domains: [String]) { + super.init(store: FireproofDomainsStoreMock()) + + for domain in domains { + super.add(domain: domain) + } + } + } +} + +// MARK: - TestData +private struct TestData: Codable { + let fireButtonFireproofing: FireButtonFireproofing +} + +// MARK: - FireButtonFireproofing +private struct FireButtonFireproofing: Codable { + let name, desc: String + let fireproofedSites: [String] + let tests: [Test] +} + +// MARK: - Test +private struct Test: Codable { + let name, cookieDomain, cookieName: String + let expectCookieRemoved: Bool + let exceptPlatforms: [String] +} From c41427d655a0797dfe3f293458a26205a6ea75a6 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Wed, 11 Jan 2023 10:26:29 +0000 Subject: [PATCH 22/29] Add dependabot (#903) Task/Issue URL: https://app.asana.com/0/0/1203456821249391/f Description: Add dependabot to keep submodules up to date. --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..39efafc05b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gitsubmodule" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" From f62e4221628d3462124bc751c22311d71d12fc55 Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:35:45 +0100 Subject: [PATCH 23/29] Fix of Bitwarden connection problem (#924) Task/Issue URL: https://app.asana.com/0/1177771139624306/1203672423028788/f Description: Fix of the communication with Bitwarden caused by not injecting correct arguments to SecureVaultManager. --- DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift b/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift index e37f5c9a7c..7bc5768799 100644 --- a/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift +++ b/DuckDuckGo/Browser Tab/Extensions/AutofillTabExtension.swift @@ -29,7 +29,7 @@ final class AutofillTabExtension: TabExtension { } static var vaultManagerProvider: (SecureVaultManagerDelegate) -> AutofillSecureVaultDelegate = { delegate in - let manager = SecureVaultManager() + let manager = SecureVaultManager(passwordManager: PasswordManagerCoordinator.shared) manager.delegate = delegate return manager } From 48f2639bb7af1730d7269b6c2a975bd321b7df23 Mon Sep 17 00:00:00 2001 From: Tomas Strba <57389842+tomasstrba@users.noreply.github.com> Date: Thu, 12 Jan 2023 11:17:22 +0100 Subject: [PATCH 24/29] Fix of AvailableInputTypes when username empty (#922) Task/Issue URL: https://app.asana.com/0/0/1203676618149654/f Description: This PR fixes autofill in case of empty login item username --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 83cb88fc2f..4240f45698 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5873,7 +5873,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 42.0.0; + version = 42.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { From 9480d552b8af011190cdbd4b780f0537d20aef96 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Jan 2023 19:06:24 +0100 Subject: [PATCH 25/29] =?UTF-8?q?Use=20=E2=80=98Settings=E2=80=99=20in=20p?= =?UTF-8?q?lace=20of=20=E2=80=98Preferences=E2=80=99=20on=20macOS=20Ventur?= =?UTF-8?q?a=20(#925)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1177771139624306/1203695832882334/f Description: Update copy in Safari logins import dialog and permissions context menu to use “Settings” when on macOS 13 and above. --- DuckDuckGo/Common/Localizables/UserText.swift | 6 +++++- DuckDuckGo/Data Import/View/DataImport.storyboard | 9 +++++---- .../View/FileImportViewController.swift | 15 +++++++++++++++ .../Permissions/View/PermissionContextMenu.swift | 12 +++++++++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 744b59b812..8ac2638e11 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -242,7 +242,8 @@ struct UserText { static let permissionAppPermissionDisabledFormat = NSLocalizedString("permission.disabled.app", value: "%@ access is disabled for %@", comment: "The app (DuckDuckGo: %@ 2) has no access permission to (%@ 1) media device") static let permissionGeolocationServicesDisabled = NSLocalizedString("permission.disabled.system", value: "System location services are disabled", comment: "Geolocation Services are disabled in System Preferences") - static let permissionOpenSystemPreferences = NSLocalizedString("permission.open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App)") + static let permissionOpenSystemPreferences = NSLocalizedString("permission.open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") + static let permissionOpenSystemSettings = NSLocalizedString("permission.open.settings", value: "Open System Settings", comment: "Open System Settings (to re-enable permission for the App) (macOS 13 and above)") static let permissionPopupTitle = NSLocalizedString("permission.popup.title", value: "Blocked Pop-ups", comment: "List of blocked popups Title") static let permissionPopupOpenFormat = NSLocalizedString("permission.popup.open.format", value: "%@", comment: "Open %@ URL Pop-up") @@ -305,6 +306,9 @@ struct UserText { // MARK: - Login Import & Export + static let safariPreferences = NSLocalizedString("import.logins.safari.preferences", value: "Preferences", comment: "Title of the Safari Preferences menu (up to and including macOS 12)") + static let safariSettings = NSLocalizedString("import.logins.safari.settings", value: "Settings", comment: "Title of the Safari Settings menu (macOS 13 and above)") + static let importLoginsCSV = NSLocalizedString("import.logins.csv.title", value: "CSV Logins File", comment: "Title text for the CSV importer") static let importBookmarksHTML = NSLocalizedString("import.bookmarks.html.title", value: "HTML Bookmarks File", comment: "Title text for the HTML Bookmarks importer") static let importBookmarksSelectHTMLFile = NSLocalizedString("import.bookmarks.select-html-file", value: "Select HTML Bookmarks File…", comment: "Button text for selecting HTML Bookmarks file") diff --git a/DuckDuckGo/Data Import/View/DataImport.storyboard b/DuckDuckGo/Data Import/View/DataImport.storyboard index b7cb7e9e4b..79c4f876a3 100644 --- a/DuckDuckGo/Data Import/View/DataImport.storyboard +++ b/DuckDuckGo/Data Import/View/DataImport.storyboard @@ -1,8 +1,8 @@ - + - + @@ -771,8 +771,8 @@ Gw - - + + @@ -1652,6 +1652,7 @@ Gw + diff --git a/DuckDuckGo/Data Import/View/FileImportViewController.swift b/DuckDuckGo/Data Import/View/FileImportViewController.swift index 5762239c1b..1864db5189 100644 --- a/DuckDuckGo/Data Import/View/FileImportViewController.swift +++ b/DuckDuckGo/Data Import/View/FileImportViewController.swift @@ -56,6 +56,8 @@ final class FileImportViewController: NSViewController { @IBOutlet var lastPassInfoView: NSView! @IBOutlet var onePasswordInfoView: NSView! + @IBOutlet var safariSettingsTextField: NSTextField! + var importSource: DataImport.Source = .csv { didSet { if oldValue != importSource { @@ -81,9 +83,22 @@ final class FileImportViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() + setUpSafariImportInstructions() renderCurrentState() } + private func setUpSafariImportInstructions() { + let safariSettingsTitle: String = { + if #available(macOS 13.0, *) { + return UserText.safariSettings + } else { + return UserText.safariPreferences + } + }() + + safariSettingsTextField.stringValue = "Safari → \(safariSettingsTitle)" + } + private func renderCurrentState() { guard isViewLoaded else { return } render(state: currentImportState) diff --git a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift index c924d827a3..e89a51a04e 100644 --- a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift +++ b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift @@ -327,9 +327,15 @@ private extension NSMenuItem { } static func openSystemPreferences(for permission: PermissionType, target: PermissionContextMenu) -> NSMenuItem { - let item = NSMenuItem(title: UserText.permissionOpenSystemPreferences, - action: #selector(PermissionContextMenu.openSystemPreferences), - keyEquivalent: "") + let title: String = { + if #available(macOS 13.0, *) { + return UserText.permissionOpenSystemSettings + } else { + return UserText.permissionOpenSystemPreferences + } + }() + + let item = NSMenuItem(title: title, action: #selector(PermissionContextMenu.openSystemPreferences), keyEquivalent: "") item.representedObject = permission item.target = target return item From b6c303bb2a9a4a4d50073b67d6c361f8040786c6 Mon Sep 17 00:00:00 2001 From: Alistair Brown Date: Thu, 12 Jan 2023 19:06:30 +0000 Subject: [PATCH 26/29] Bump find-in-page version (#889) --- Submodules/duckduckgo-find-in-page | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submodules/duckduckgo-find-in-page b/Submodules/duckduckgo-find-in-page index 495697ed5b..198014de6f 160000 --- a/Submodules/duckduckgo-find-in-page +++ b/Submodules/duckduckgo-find-in-page @@ -1 +1 @@ -Subproject commit 495697ed5b08b761d7e63003db73da4309175d20 +Subproject commit 198014de6f66c57644c441bc7c8a20c95b1b3b64 From d91f7fe7898bf105a2d5976c7eead51ba904d49c Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 13 Jan 2023 02:59:28 -0800 Subject: [PATCH 27/29] Remove the QuackDev reference. (#926) --- DuckDuckGo/Email/EmailUrlExtensions.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/DuckDuckGo/Email/EmailUrlExtensions.swift b/DuckDuckGo/Email/EmailUrlExtensions.swift index abcb4497bf..a4b9c287ff 100644 --- a/DuckDuckGo/Email/EmailUrlExtensions.swift +++ b/DuckDuckGo/Email/EmailUrlExtensions.swift @@ -25,16 +25,8 @@ extension EmailUrls { static let emailProtectionLink = "https://duckduckgo.com/email" } - private struct DevUrl { - static let emailProtectionLink = "https://quackdev.duckduckgo.com/email" - } - var emailProtectionLink: URL { - #if DEBUG - return URL(string: DevUrl.emailProtectionLink)! - #else return URL(string: Url.emailProtectionLink)! - #endif } func isDuckDuckGoEmailProtection(url: URL) -> Bool { From df2ceb6c7055d940a5a9430addc77c5e3070ea13 Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Fri, 13 Jan 2023 11:17:15 +0000 Subject: [PATCH 28/29] update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 +-- DuckDuckGo/Content Blocker/macos-config.json | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift index 0e0f948249..7977957965 100644 --- a/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"4799b1d0bde3b1c4aa23d675b8f7fc2f\"" - public static let embeddedDataSHA = "c8642092e80a136aae7763d34f321af771ddb69277030c8d00d8eb8c2a5bd0cc" + public static let embeddedDataETag = "\"bfeb56a9a00c21697fb5829e3a39c3bc\"" + public static let embeddedDataSHA = "9929279a1b270e7f740fd6f7cdb552b05fbf1e3195ef7f01dae90e642123365d" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/Content Blocker/macos-config.json b/DuckDuckGo/Content Blocker/macos-config.json index 712e5239f6..b62cb23e72 100644 --- a/DuckDuckGo/Content Blocker/macos-config.json +++ b/DuckDuckGo/Content Blocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1672428721322, + "version": 1673546710965, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -209,13 +209,17 @@ { "domain": "qubushotel.com", "reason": "Homepage UI elements appear squashed together, preventing interaction with the site." + }, + { + "domain": "digid.nl", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/602" } ], "omitVersionSites": [] }, "exceptions": [], "state": "disabled", - "hash": "acbe310f15d778d2e46f5266e319afcc" + "hash": "f303f2b5c222373004029d0dd657fe56" }, "duckPlayer": { "exceptions": [], @@ -251,6 +255,10 @@ { "domain": "wiwo.de", "reason": "https://github.com/duckduckgo/privacy-configuration/issues/592" + }, + { + "domain": "metro.co.uk", + "reason": "https://github.com/duckduckgo/privacy-configuration/issues/592" } ], "settings": { @@ -845,6 +853,19 @@ } ] }, + { + "domain": "xfinity.com", + "rules": [ + { + "selector": ".f-gpc-flyout", + "type": "hide" + }, + { + "selector": ".f-gpc-banner", + "type": "hide" + } + ] + }, { "domain": "first-party.site", "rules": [ @@ -890,7 +911,7 @@ ] }, "state": "enabled", - "hash": "de63929cb4071637408a65f79ecce089" + "hash": "713823f7d7e219814cb96e879736f4d3" }, "fingerprintingAudio": { "state": "disabled", @@ -1090,6 +1111,14 @@ "state": "enabled", "hash": "52857469413a66e8b0c7b00de5589162" }, + "requestFilterer": { + "state": "disabled", + "exceptions": [], + "settings": { + "windowInMs": 0 + }, + "hash": "9439c856372a09f0cfdc9e2e0fd086fd" + }, "trackerAllowlist": { "state": "enabled", "settings": { From 24d849d8f991b6458b64a3922a50f2d9b1a4c2fa Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Fri, 13 Jan 2023 11:36:37 +0000 Subject: [PATCH 29/29] update version --- Configuration/Version.xcconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index fed02c2293..f044674f85 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1,2 @@ -MARKETING_VERSION = 0.31.4 +MARKETING_VERSION = 0.31.5 +