diff --git a/DuckDuckGo/Application/AppConfigurationURLProvider.swift b/DuckDuckGo/Application/AppConfigurationURLProvider.swift index 5ca622f3ec..62025ff550 100644 --- a/DuckDuckGo/Application/AppConfigurationURLProvider.swift +++ b/DuckDuckGo/Application/AppConfigurationURLProvider.swift @@ -21,6 +21,32 @@ import Configuration struct AppConfigurationURLProvider: ConfigurationURLProviding { + // MARK: - Debug + + internal init(customPrivacyConfiguration: URL? = nil) { + if let customPrivacyConfiguration { + // Overwrite custom privacy configuration if provided + self.customPrivacyConfiguration = customPrivacyConfiguration.absoluteString + } + // Otherwise use the default or already stored custom configuration + } + + @UserDefaultsWrapper(key: .customConfigurationUrl, defaultValue: nil) + private var customPrivacyConfiguration: String? + + private var customPrivacyConfigurationUrl: URL? { + if let customPrivacyConfiguration { + return URL(string: customPrivacyConfiguration) + } + return nil + } + + mutating func resetToDefaultConfigurationUrl() { + self.customPrivacyConfiguration = nil + } + + // MARK: - Main + func url(for configuration: Configuration) -> URL { // URLs for privacyConfiguration and trackerDataSet shall match the ones in update_embedded.sh. // Danger checks that the URLs match on every PR. If the code changes, the regex that Danger uses may need an update. @@ -28,7 +54,7 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { case .bloomFilterBinary: return URL(string: "https://staticcdn.duckduckgo.com/https/https-mobile-v2-bloom.bin")! case .bloomFilterSpec: return URL(string: "https://staticcdn.duckduckgo.com/https/https-mobile-v2-bloom-spec.json")! case .bloomFilterExcludedDomains: return URL(string: "https://staticcdn.duckduckgo.com/https/https-mobile-v2-false-positives.json")! - case .privacyConfiguration: return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json")! + case .privacyConfiguration: return customPrivacyConfigurationUrl ?? URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json")! case .surrogates: return URL(string: "https://staticcdn.duckduckgo.com/surrogates.txt")! case .trackerDataSet: return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/v5/current/macos-tds.json")! // In archived repo, to be refactored shortly (https://staticcdn.duckduckgo.com/useragents/social_ctp_configuration.json) diff --git a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift index d232fd5d08..6c0bd356bd 100644 --- a/DuckDuckGo/Common/Extensions/NSAlertExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAlertExtension.swift @@ -201,6 +201,21 @@ extension NSAlert { return alert } + static func customConfigurationAlert(configurationUrl: String) -> NSAlert { + let alert = NSAlert() + alert.messageText = "Set custom configuration URL:" + alert.addButton(withTitle: UserText.ok) + alert.addButton(withTitle: UserText.cancel) + let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) + textField.maximumNumberOfLines = 1 + textField.lineBreakMode = .byTruncatingTail + textField.stringValue = configurationUrl + alert.accessoryView = textField + alert.window.initialFirstResponder = alert.accessoryView + textField.currentEditor()?.selectAll(nil) + return alert + } + @discardableResult func runModal() async -> NSApplication.ModalResponse { await withCheckedContinuation { continuation in diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index d65a1b7c87..4808e95d24 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -135,6 +135,7 @@ public struct UserDefaultsWrapper { case loggingCategories = "logging.categories" case firstLaunchDate = "first.app.launch.date" + case customConfigurationUrl = "custom.configuration.url" // Data Broker Protection diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index bd8440f874..a486bb4365 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -59,7 +59,7 @@ final class ConfigurationManager { static let queue: DispatchQueue = DispatchQueue(label: "Configuration Manager") @UserDefaultsWrapper(key: .configLastUpdated, defaultValue: .distantPast) - private var lastUpdateTime: Date + private(set) var lastUpdateTime: Date private var timerCancellable: AnyCancellable? private var lastRefreshCheckTime: Date = Date() diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index ab8bafbc28..08439a3db4 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -23,6 +23,7 @@ import Combine import OSLog // swiftlint:disable:this enforce_os_log_wrapper import SwiftUI import WebKit +import Configuration #if NETWORK_PROTECTION import NetworkProtection @@ -93,6 +94,8 @@ import Subscription // MARK: - Debug private var loggingMenu: NSMenu? + let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil) + let configurationDateAndTimeMenuItem = NSMenuItem(title: "Configuration URL", action: nil) // MARK: - Help @@ -380,6 +383,7 @@ import Subscription updateBookmarksBarMenuItem() updateShortcutMenuItems() updateLoggingMenuItems() + updateRemoteConfigurationInfo() } // MARK: - Bookmarks @@ -530,6 +534,7 @@ import Subscription // MARK: - Debug + // swiftlint:disable:next function_body_length private func setupDebugMenu() -> NSMenu { let debugMenu = NSMenu(title: "Debug") { NSMenuItem(title: "Reset Data") { @@ -555,7 +560,15 @@ import Subscription NSMenuItem(title: "Show Pop Up Window", action: #selector(MainViewController.showPopUpWindow)) } NSMenuItem(title: "Remote Configuration") { - NSMenuItem(title: "Fetch Configuration Now", action: #selector(MainViewController.fetchConfigurationNow)) + customConfigurationUrlMenuItem + configurationDateAndTimeMenuItem + NSMenuItem.separator() + NSMenuItem(title: "Reload Configuration Now", action: #selector(MainViewController.reloadConfigurationNow)) + NSMenuItem(title: "Set custom configuration URL…", action: #selector(MainViewController.setCustomConfigurationURL)) + NSMenuItem(title: "Reset configuration to default", action: #selector(MainViewController.resetConfigurationToDefault)) + } + NSMenuItem(title: "User Scripts") { + NSMenuItem(title: "Remove user scripts from selected tab", action: #selector(MainViewController.removeUserScripts)) } NSMenuItem(title: "Sync") .submenu(SyncDebugMenu()) @@ -621,6 +634,12 @@ import Subscription } } + private func updateRemoteConfigurationInfo() { + let dateString = DateFormatter.localizedString(from: ConfigurationManager.shared.lastUpdateTime, dateStyle: .short, timeStyle: .medium) + configurationDateAndTimeMenuItem.title = "Last Update Time: \(dateString)" + customConfigurationUrlMenuItem.title = "Configuration URL: \(AppConfigurationURLProvider().url(for: .privacyConfiguration).absoluteString)" + } + @objc private func loggingMenuItemAction(_ sender: NSMenuItem) { guard let category = sender.identifier?.rawValue else { return } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 17ca80844f..709c304cdb 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -20,6 +20,7 @@ import BrowserServicesKit import Cocoa import Common import WebKit +import Configuration // Actions are sent to objects of responder chain @@ -724,10 +725,52 @@ extension MainViewController { EmailManager().resetEmailProtectionInContextPrompt() } - @objc func fetchConfigurationNow(_ sender: Any?) { + @objc func removeUserScripts(_ sender: Any?) { + tabCollectionViewModel.selectedTab?.userContentController?.cleanUpBeforeClosing() + tabCollectionViewModel.selectedTab?.reload() + os_log("User scripts removed from the current tab", type: .info) + } + + @objc func reloadConfigurationNow(_ sender: Any?) { + OSLog.loggingCategories.insert(OSLog.AppCategories.config.rawValue) + ConfigurationManager.shared.forceRefresh() } + private func setConfigurationUrl(_ configurationUrl: URL?) { + OSLog.loggingCategories.insert(OSLog.AppCategories.config.rawValue) + + var configurationProvider = AppConfigurationURLProvider(customPrivacyConfiguration: configurationUrl) + if configurationUrl == nil { + configurationProvider.resetToDefaultConfigurationUrl() + } + Configuration.setURLProvider(configurationProvider) + ConfigurationManager.shared.forceRefresh() + if let configurationUrl { + os_log("New configuration URL set to \(configurationUrl.absoluteString)", type: .info) + } else { + os_log("New configuration URL reset to default", type: .info) + } + } + + @objc func setCustomConfigurationURL(_ sender: Any?) { + let currentConfigurationURL = AppConfigurationURLProvider().url(for: .privacyConfiguration).absoluteString + let alert = NSAlert.customConfigurationAlert(configurationUrl: currentConfigurationURL) + if alert.runModal() != .cancel { + guard let textField = alert.accessoryView as? NSTextField, + let newConfigurationUrl = URL(string: textField.stringValue) else { + os_log("Failed to set custom configuration URL", type: .error) + return + } + + setConfigurationUrl(newConfigurationUrl) + } + } + + @objc func resetConfigurationToDefault(_ sender: Any?) { + setConfigurationUrl(nil) + } + // MARK: - Developer Tools @objc func toggleDeveloperTools(_ sender: Any?) { diff --git a/Submodules/privacy-reference-tests b/Submodules/privacy-reference-tests index 2e73221f9b..0d23f76801 160000 --- a/Submodules/privacy-reference-tests +++ b/Submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 2e73221f9b5d872e05199db6b29f140406c909ae +Subproject commit 0d23f76801c2e73ae7d5ed7daa4af4aca5beec73