diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2e85fd1a32..5e81db4c81 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5268,7 +5268,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "REVIEW=1"; @@ -5277,7 +5277,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.review; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "DuckDuckGo Review"; @@ -5419,7 +5419,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5427,7 +5427,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -5619,7 +5619,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5627,7 +5627,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -5872,7 +5872,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5880,7 +5880,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -5901,7 +5901,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5909,7 +5909,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -6034,7 +6034,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "REVIEW=1"; @@ -6043,7 +6043,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.review; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "DuckDuckGo Review"; @@ -6212,7 +6212,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 24.0.0; + version = 27.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo/API/APIRequest.swift b/DuckDuckGo/API/APIRequest.swift index 447d8ecc47..2ed065349d 100644 --- a/DuckDuckGo/API/APIRequest.swift +++ b/DuckDuckGo/API/APIRequest.swift @@ -90,7 +90,7 @@ enum APIRequest { var etag = httpResponse?.headerValue(for: APIHeaders.Name.etag) // Handle weak etags - etag = etag?.drop(prefix: "W/") + etag = etag?.dropping(prefix: "W/") completion(Response(data: data, etag: etag, urlResponse: response), nil) } } @@ -105,7 +105,7 @@ enum APIRequest { headers: HTTPHeaders = APIHeaders().defaultHeaders, timeoutInterval: TimeInterval = 60.0) -> URLRequest { let url = (try? parameters?.reduce(url) { partialResult, parameter in - try partialResult.addParameter( + try partialResult.appendingParameter( name: parameter.key, value: parameter.value, allowedReservedCharacters: allowedQueryReservedCharacters diff --git a/DuckDuckGo/App Delegate/CopyHandler.swift b/DuckDuckGo/App Delegate/CopyHandler.swift index 7dc525dcdd..384c9b6b99 100644 --- a/DuckDuckGo/App Delegate/CopyHandler.swift +++ b/DuckDuckGo/App Delegate/CopyHandler.swift @@ -33,7 +33,7 @@ final class CopyHandler: NSObject { NSPasteboard.general.clearContents() NSPasteboard.general.setString(selectedText, forType: .string) - if let urlString = URL(trimmedAddressBarString: selectedText.trimmingWhitespaces())?.absoluteString, + if let urlString = URL(trimmedAddressBarString: selectedText.trimmingWhitespace())?.absoluteString, urlString == selectedText { NSPasteboard.general.setString(urlString, forType: .URL) } diff --git a/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json index 856b2fdda0..665220ff0c 100644 --- a/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json @@ -4,28 +4,10 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.600", - "blue" : "0xEF", - "green" : "0x69", - "red" : "0x39" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "contrast", - "value" : "high" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.600", - "blue" : "0.937", - "green" : "0.412", - "red" : "0.224" + "alpha" : "1.000", + "blue" : "0xFC", + "green" : "0x7A", + "red" : "0x3A" } }, "idiom" : "universal" diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift index 118d376dc4..272bc41115 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift @@ -48,7 +48,7 @@ public final class CookieConsentPopover { viewController.delegate = self } - public func close(animated: Bool) { + public func close(animated: Bool, completion: (() -> Void)? = nil) { guard let overlayWindow = windowController.window else { return } @@ -57,6 +57,7 @@ public final class CookieConsentPopover { let removeWindow = { overlayWindow.parent?.removeChildWindow(overlayWindow) overlayWindow.orderOut(nil) + completion?() } if animated { diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift index 62f3466b79..299139e64f 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift @@ -22,21 +22,51 @@ final class CookieConsentPopoverManager: CookieConsentPopoverDelegate { var completion: ((Bool) -> Void)? weak var currentTab: Tab? - lazy var popOver: CookieConsentPopover = { - let popover = CookieConsentPopover() - popover.delegate = self - return popover - }() + private(set) var popOver: CookieConsentPopover? func cookieConsentPopover(_ popOver: CookieConsentPopover, didFinishWithResult result: Bool) { - popOver.close(animated: true) + 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 + } - func show(on view: NSView, animated: Bool, result: @escaping (Bool) -> Void) { - popOver.show(on: view, animated: true) - self.completion = result + popOver.show(on: view, animated: animated) + if let result = result { + self.completion = result + } + } + + func close(animated: Bool) { + guard let popOver = popOver else { + return + } + + popOver.close(animated: animated) + } + + private func preparePopover() { + // If the tab was closed, we want to start the animation again + if currentTab == nil { + popOver = nil + } + + guard popOver == nil else { + return + } + + popOver = CookieConsentPopover() + popOver?.delegate = self } } diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index dd8297c59d..573ed9ff6f 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -147,13 +147,7 @@ public final class ContentOverlayViewController: NSViewController, EmailManagerR completion: @escaping (Data?, Error?) -> Void) { let currentQueue = OperationQueue.current - let finalURL: URL - - if let parameters = parameters { - finalURL = (try? url.addParameters(parameters)) ?? url - } else { - finalURL = url - } + let finalURL = (try? url.appendingParameters(parameters ?? [:])) ?? url var request = URLRequest(url: finalURL, timeoutInterval: timeoutInterval) request.allHTTPHeaderFields = headers diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index e718be5718..4cb1f939f6 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -202,7 +202,7 @@ final class LocalBookmarkStore: BookmarkStore { try self.context.save() } catch { assertionFailure("LocalBookmarkStore: Saving of context failed") - DispatchQueue.main.async { completion(false, error) } + DispatchQueue.main.async { completion(true, error) } return } @@ -232,7 +232,7 @@ final class LocalBookmarkStore: BookmarkStore { try self.context.save() } catch { assertionFailure("LocalBookmarkStore: Saving of context failed") - DispatchQueue.main.async { completion(false, error) } + DispatchQueue.main.async { completion(true, error) } } DispatchQueue.main.async { completion(true, nil) } @@ -405,7 +405,7 @@ final class LocalBookmarkStore: BookmarkStore { assertionFailure("LocalBookmarkStore: Saving of context failed") } - DispatchQueue.main.async { completion(false, error) } + DispatchQueue.main.async { completion(true, error) } return } diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift index 5aeffadb88..dee54558e9 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift @@ -80,7 +80,7 @@ struct BookmarkViewModel { preconditionFailure("\(#file): Attempted to provide representing character for non-Bookmark") } - return bookmark.url.host?.dropWWW().first?.uppercased() ?? "-" + return bookmark.url.host?.droppingWwwPrefix().first?.uppercased() ?? "-" } } diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index e71e5af7e0..7c26e75c81 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -692,7 +692,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { // Add to local history if let host = url.host, !host.isEmpty { - localHistory.insert(host.dropWWW()) + localHistory.insert(host.droppingWwwPrefix()) } } diff --git a/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift index 8350040eca..9b8a7692d2 100644 --- a/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift @@ -114,7 +114,7 @@ final class BrowserTabViewController: NSViewController { private func windowWillClose(_ notification: NSNotification) { self.removeWebViewFromHierarchy() } - + private func subscribeToSelectedTabViewModel() { tabCollectionViewModel.$selectedTabViewModel .sink { [weak self] selectedTabViewModel in @@ -128,12 +128,14 @@ final class BrowserTabViewController: NSViewController { } .store(in: &cancellables) } - + private func showCookieConsentPopoverIfNecessary(_ selectedTabViewModel: TabViewModel?) { - if selectedTabViewModel?.tab == cookieConsentPopoverManager.currentTab { - cookieConsentPopoverManager.popOver.show(on: view, animated: false) + if let selectedTab = selectedTabViewModel?.tab, + let cookiePopoverTab = cookieConsentPopoverManager.currentTab, + selectedTab == cookiePopoverTab { + cookieConsentPopoverManager.show(on: view, animated: false) } else { - cookieConsentPopoverManager.popOver.close(animated: false) + cookieConsentPopoverManager.close(animated: false) } } @@ -475,7 +477,7 @@ extension BrowserTabViewController: TabDelegate { func tab(_ tab: Tab, promptUserForCookieConsent result: @escaping (Bool) -> Void) { cookieConsentPopoverManager.show(on: view, animated: true, result: result) - cookieConsentPopoverManager.currentTab = tabViewModel?.tab + cookieConsentPopoverManager.currentTab = tab } func tabWillStartNavigation(_ tab: Tab, isUserInitiated: Bool) { diff --git a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift index e85c8710d1..d2331282f5 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift @@ -194,7 +194,7 @@ final class TabViewModel { guard !errorViewState.isVisible else { let failingUrl = tab.error?.failingUrl addressBarString = failingUrl?.absoluteString ?? "" - passiveAddressBarString = failingUrl?.host?.drop(prefix: URL.HostPrefix.www.separated()) ?? "" + passiveAddressBarString = failingUrl?.host?.droppingWwwPrefix() ?? "" return } diff --git a/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift b/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift index f8bd0b2296..eebb360929 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift @@ -117,8 +117,8 @@ final class WebViewStateObserver: NSObject { } private func updateTitle() { - if webView?.title?.trimmingWhitespaces().isEmpty ?? true { - tabViewModel?.tab.title = webView?.url?.host?.dropWWW() + if webView?.title?.trimmingWhitespace().isEmpty ?? true { + tabViewModel?.tab.title = webView?.url?.host?.droppingWwwPrefix() return } diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index ba03fa0299..984ca55539 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -53,7 +53,7 @@ extension FileManager { } let ownerDirectory = destURL.deletingLastPathComponent() - let fileNameWithoutExtension = destURL.lastPathComponent.drop(suffix: suffix) + let fileNameWithoutExtension = destURL.lastPathComponent.dropping(suffix: suffix) for copy in 0... { let destURL: URL = { diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index 14701a9b39..7114d55f33 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -18,31 +18,12 @@ import Foundation import os.log - -typealias RegEx = NSRegularExpression - -func regex(_ pattern: String, _ options: NSRegularExpression.Options = []) -> NSRegularExpression { - // swiftlint:disable force_try - return try! NSRegularExpression(pattern: pattern, options: options) - // swiftlint:enable force_try -} - -private extension RegEx { - // from https://stackoverflow.com/a/25717506/73479 - static let hostName = regex("^(((?!-)[A-Za-z0-9-]{1,63}(? String { - trimmingCharacters(in: .whitespacesAndNewlines) - } - func nsRange(from range: Range? = nil) -> NSRange { if let range = range { return NSRange(location: self[.. Bool { - let matches = regex.matches(in: self, options: .anchored, range: NSRange(location: 0, length: self.utf16.count)) - return matches.count == 1 - } - // MARK: - URL var url: URL? { @@ -75,31 +49,12 @@ extension String { static let localhost = "localhost" - var isValidHost: Bool { - return isValidHostname || isValidIpHost - } - - var isValidHostname: Bool { - if self == Self.localhost { - return true - } - return matches(.hostName) - } - - var isValidIpHost: Bool { - return matches(.ipAddress) - } - func dropSubdomain() -> String? { let parts = components(separatedBy: ".") guard parts.count > 1 else { return nil } return parts.dropFirst().joined(separator: ".") } - func dropWWW() -> String { - self.drop(prefix: URL.HostPrefix.www.separated()) - } - static func uniqueFilename(for fileType: UTType? = nil) -> String { let fileName = UUID().uuidString @@ -122,14 +77,4 @@ extension String { return hasPrefix(other) || other.hasPrefix(self) } - func drop(prefix: String) -> String { - return hasPrefix(prefix) ? String(dropFirst(prefix.count)) : self - } - - // MARK: - Suffix - - func drop(suffix: String) -> String { - return hasSuffix(suffix) ? String(dropLast(suffix.count)) : self - } - } diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 34a6046610..4253ab0d25 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -20,6 +20,14 @@ import Foundation import os.log import BrowserServicesKit +extension URL.NavigationalScheme { + + static var validSchemes: [URL.NavigationalScheme] { + return [.http, .https, .file] + } + +} + extension URL { // MARK: - Local @@ -45,9 +53,8 @@ extension URL { } do { - var searchUrl = Self.duckDuckGo - searchUrl = try searchUrl.addParameter(name: DuckDuckGoParameters.search.rawValue, value: trimmedQuery) - return searchUrl + return try Self.duckDuckGo + .appendingParameter(name: DuckDuckGoParameters.search.rawValue, value: trimmedQuery) } catch let error { os_log("URL extension: %s", type: .error, error.localizedDescription) return nil @@ -55,7 +62,7 @@ extension URL { } static func makeURL(from addressBarString: String) -> URL? { - let trimmed = addressBarString.trimmingWhitespaces() + let trimmed = addressBarString.trimmingWhitespace() if let addressBarUrl = URL(trimmedAddressBarString: trimmed), addressBarUrl.isValid { return addressBarUrl @@ -69,105 +76,6 @@ extension URL { return nil } - /// URL and URLComponents can't cope with emojis and international characters so this routine does some manual processing while trying to - /// retain the input as much as possible. - init?(trimmedAddressBarString: String) { - var s = trimmedAddressBarString - - // Creates URL even if user enters one slash "/" instead of two slashes "//" after the hypertext scheme component - if let scheme = NavigationalScheme.hypertextSchemes.first(where: { s.hasPrefix($0.rawValue + ":/") }), - !s.hasPrefix(scheme.separated()) { - s = scheme.separated() + s.dropFirst(scheme.separated().count - 1) - } - - if let url = URL(string: s) { - // if URL has domain:port or user:password@domain mistakengly interpreted as a scheme - if let urlWithScheme = URL(string: NavigationalScheme.http.separated() + s), - urlWithScheme.port != nil || urlWithScheme.user != nil { - // could be a local domain but user needs to use the protocol to specify that - // make exception for "localhost" - guard urlWithScheme.host?.contains(".") == true || urlWithScheme.host == .localhost else { return nil } - self = urlWithScheme - return - - } else if url.scheme != nil { - self = url - return - - } else if let hostname = s.split(separator: "/").first { - guard hostname.contains(".") || String(hostname) == .localhost else { - // could be a local domain but user needs to use the protocol to specify that - return nil - } - } else { - return nil - } - - s = NavigationalScheme.http.separated() + s - } - - self.init(punycodeEncodedString: s) - } - - private init?(punycodeEncodedString: String) { - var s = punycodeEncodedString - let scheme: String - - if s.hasPrefix(URL.NavigationalScheme.http.separated()) { - scheme = URL.NavigationalScheme.http.separated() - } else if s.hasPrefix(URL.NavigationalScheme.https.separated()) { - scheme = URL.NavigationalScheme.https.separated() - } else if !s.contains(".") { - return nil - } else { - scheme = URL.NavigationalScheme.http.separated() - s = scheme + s - } - - let urlAndQuery = s.split(separator: "?") - guard !urlAndQuery.isEmpty, !urlAndQuery[0].contains(" ") else { - return nil - } - - var query = "" - if urlAndQuery.count > 1 { - // replace spaces with %20 in query values - do { - struct Throwable: Error {} - query = try "?" + urlAndQuery[1].components(separatedBy: "&").map { component in - try component.components(separatedBy: "=").enumerated().map { (idx, component) -> String in - if idx == 0 { // name - // don't allow spaces in query names - guard !component.contains(" ") else { throw Throwable() } - return component - } else { // value - return component.replacingOccurrences(of: " ", with: "%20") - } - }.joined(separator: "=") - }.joined(separator: "&") - } catch { - return nil - } - } - - let componentsWithoutQuery = urlAndQuery[0].split(separator: "/").dropFirst().map(String.init) - guard !componentsWithoutQuery.isEmpty else { - return nil - } - - let host = componentsWithoutQuery[0].punycodeEncodedHostname - - let encodedPath = componentsWithoutQuery - .dropFirst() - .map { $0.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlPathAllowed) ?? $0 } - .joined(separator: "/") - - let hostPathSeparator = !encodedPath.isEmpty || urlAndQuery[0].hasSuffix("/") ? "/" : "" - let url = scheme + host + hostPathSeparator + encodedPath + query - - self.init(string: url) - } - static func makeURL(fromSuggestionPhrase phrase: String) -> URL? { guard let url = URL(trimmedAddressBarString: phrase), url.isValid else { return nil } return url @@ -224,46 +132,28 @@ extension URL { static func searchAtb(atbWithVariant: String, setAtb: String) -> URL? { return try? Self.initialAtb - .addParameter(name: DuckDuckGoParameters.ATB.atb, value: atbWithVariant) - .addParameter(name: DuckDuckGoParameters.ATB.setAtb, value: setAtb) + .appendingParameters([ + DuckDuckGoParameters.ATB.atb: atbWithVariant, + DuckDuckGoParameters.ATB.setAtb: setAtb + ]) } static func appRetentionAtb(atbWithVariant: String, setAtb: String) -> URL? { return try? Self.initialAtb - .addParameter(name: DuckDuckGoParameters.ATB.activityType, value: DuckDuckGoParameters.ATB.appUsageValue) - .addParameter(name: DuckDuckGoParameters.ATB.atb, value: atbWithVariant) - .addParameter(name: DuckDuckGoParameters.ATB.setAtb, value: setAtb) + .appendingParameters([ + DuckDuckGoParameters.ATB.activityType: DuckDuckGoParameters.ATB.appUsageValue, + DuckDuckGoParameters.ATB.atb: atbWithVariant, + DuckDuckGoParameters.ATB.setAtb: setAtb + ]) } static func exti(forAtb atb: String) -> URL? { let extiUrl = URL(string: Self.exti)! - return try? extiUrl.addParameter(name: DuckDuckGoParameters.ATB.atb, value: atb) + return try? extiUrl.appendingParameter(name: DuckDuckGoParameters.ATB.atb, value: atb) } // MARK: - Components - struct NavigationalScheme: RawRepresentable, Hashable { - let rawValue: String - - static let separator = "://" - - static let http = NavigationalScheme(rawValue: "http") - static let https = NavigationalScheme(rawValue: "https") - static let file = NavigationalScheme(rawValue: "ftp") - - func separated() -> String { - self.rawValue + Self.separator - } - - static var hypertextSchemes: [NavigationalScheme] { - return [.http, .https] - } - - static var validSchemes: [NavigationalScheme] { - return [.http, .https, .file] - } - } - enum HostPrefix: String { case www @@ -295,7 +185,7 @@ extension URL { (.none, _): break case (.some(false), true): - host = host.drop(prefix: HostPrefix.www.separated()) + host = host.dropping(prefix: HostPrefix.www.separated()) case (.some(true), false): host = HostPrefix.www.separated() + host } @@ -312,7 +202,7 @@ extension URL { let schemeRange = components.rangeOfScheme { string.replaceSubrange(schemeRange, with: "") if string.hasPrefix(URL.NavigationalScheme.separator) { - string = string.drop(prefix: URL.NavigationalScheme.separator) + string = string.dropping(prefix: URL.NavigationalScheme.separator) } } @@ -326,12 +216,12 @@ extension URL { func toString(forUserInput input: String, decodePunycode: Bool = true) -> String { let hasInputScheme = input.hasOrIsPrefix(of: self.separatedScheme ?? "") - let hasInputWww = input.drop(prefix: self.separatedScheme ?? "").hasOrIsPrefix(of: URL.HostPrefix.www.rawValue) + let hasInputWww = input.dropping(prefix: self.separatedScheme ?? "").hasOrIsPrefix(of: URL.HostPrefix.www.rawValue) let hasInputHost = (decodePunycode ? host?.idnaDecoded : host)?.hasOrIsPrefix(of: input) ?? false return self.toString(decodePunycode: decodePunycode, dropScheme: input.isEmpty || !(hasInputScheme && !hasInputHost), - needsWWW: !input.drop(prefix: self.separatedScheme ?? "").isEmpty + needsWWW: !input.dropping(prefix: self.separatedScheme ?? "").isEmpty && hasInputWww && !hasInputHost, dropTrailingSlash: !input.hasSuffix("/")) @@ -347,7 +237,7 @@ extension URL { filename = url.lastPathComponent } else { - filename = url.host?.dropWWW().replacingOccurrences(of: ".", with: "_") ?? "" + filename = url.host?.droppingWwwPrefix().replacingOccurrences(of: ".", with: "_") ?? "" } guard !filename.isEmpty else { return nil } @@ -361,18 +251,6 @@ extension URL { // MARK: - Validity - var isValid: Bool { - guard let scheme = scheme.map(NavigationalScheme.init) else { return false } - - if NavigationalScheme.hypertextSchemes.contains(scheme) { - return host?.isValidHost == true && user == nil - } - - // This effectively allows file:// and External App Scheme URLs to be entered by user - // Without this check single word entries get treated like domains - return true - } - var isDataURL: Bool { return scheme == "data" } diff --git a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift index 433c55413c..44846e89df 100644 --- a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift @@ -57,8 +57,8 @@ struct FaviconView: View { ZStack { Rectangle() - .foregroundColor(Color.forDomain(domain.dropWWW())) - Text(String(domain.dropWWW().capitalized.first ?? "?")) + .foregroundColor(Color.forDomain(domain.droppingWwwPrefix())) + Text(String(domain.droppingWwwPrefix().capitalized.first ?? "?")) .font(.title) .foregroundColor(Color.white) } diff --git a/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift index 2d1c0f465a..c207dc7903 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 = "\"f7199de225a3ac90f4bca3919e87bf22\"" - public static let embeddedDataSHA = "8fead7748dda734d7340ff92cad2e00bed92197dce894905ea3ab3a94f553dc3" + public static let embeddedDataETag = "\"447be02a9a041ab4a7cf2d8d53da119f\"" + public static let embeddedDataSHA = "0089a2b38fb66a8d9529fe864c5821724fee8f1ff7ab6a38b30460fbce9f3964" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/Content Blocker/macos-config.json b/DuckDuckGo/Content Blocker/macos-config.json index f0ddce78ae..aa02ce2797 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": 1661270003722, + "version": 1661422548137, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -73,6 +73,10 @@ { "domain": "freecodecamp.org", "reason": "Clicking 'get started' reloads the page and does not progress to the login page." + }, + { + "domain": "www.audiosciencereview.com", + "reason": "Pages on the site end up in redirect loops in Firefox." } ], "settings": { diff --git a/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift b/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift index 2aa4a4a2c4..a1778f9f79 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift @@ -77,7 +77,7 @@ struct ImportedBookmarks: Decodable { } init(name: String, type: String, urlString: String?, children: [BookmarkOrFolder]?, isDDGFavorite: Bool = false) { - self.name = name.trimmingWhitespaces() + self.name = name.trimmingWhitespace() self.type = type self.urlString = urlString self.children = children diff --git a/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift b/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift index 1909b62da0..4173e38057 100644 --- a/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift +++ b/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift @@ -338,7 +338,7 @@ private extension XMLNode { } var text: String? { - stringValue?.trimmingWhitespaces() + stringValue?.trimmingWhitespace() } var bookmark: ImportedBookmarks.BookmarkOrFolder? { diff --git a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift index 0e163b11bd..a107ff76e9 100644 --- a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift +++ b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift @@ -31,13 +31,7 @@ extension EmailManagerRequestDelegate { completion: @escaping (Data?, Error?) -> Void) { let currentQueue = OperationQueue.current - let finalURL: URL - - if let parameters = parameters { - finalURL = (try? url.addParameters(parameters)) ?? url - } else { - finalURL = url - } + let finalURL = (try? url.appendingParameters(parameters ?? [:])) ?? url var request = URLRequest(url: finalURL, timeoutInterval: timeoutInterval) request.allHTTPHeaderFields = headers diff --git a/DuckDuckGo/Favicons/Model/FaviconManager.swift b/DuckDuckGo/Favicons/Model/FaviconManager.swift index 5ad58fbdf4..05d2d960df 100644 --- a/DuckDuckGo/Favicons/Model/FaviconManager.swift +++ b/DuckDuckGo/Favicons/Model/FaviconManager.swift @@ -48,6 +48,8 @@ final class FaviconManager: FaviconManagement { private let faviconURLSession = URLSession(configuration: .ephemeral) + @Published var faviconsLoaded = false + func loadFavicons() { imageCache.loadFavicons { _ in self.imageCache.cleanOldExcept(fireproofDomains: FireproofDomains.shared, @@ -55,6 +57,7 @@ final class FaviconManager: FaviconManagement { self.referenceCache.loadReferences { _ in self.referenceCache.cleanOldExcept(fireproofDomains: FireproofDomains.shared, bookmarkManager: LocalBookmarkManager.shared) + self.faviconsLoaded = true } } } diff --git a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift index adc18a3e50..a825e5b6f0 100644 --- a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift +++ b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift @@ -113,7 +113,7 @@ final class FaviconReferenceCache { } } else if let host = documentURL.host, let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? - hostReferences[host.dropWWW()] : hostReferences["www.\(host)"]) { + hostReferences[host.droppingWwwPrefix()] : hostReferences["www.\(host)"]) { switch sizeCategory { case .small: return hostCacheEntry.smallFaviconUrl ?? hostCacheEntry.mediumFaviconUrl default: return hostCacheEntry.mediumFaviconUrl @@ -128,7 +128,7 @@ final class FaviconReferenceCache { return nil } - let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? hostReferences[host.dropWWW()] : hostReferences["www.\(host)"]) + let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? hostReferences[host.droppingWwwPrefix()] : hostReferences["www.\(host)"]) switch sizeCategory { case .small: return hostCacheEntry?.smallFaviconUrl ?? hostCacheEntry?.mediumFaviconUrl diff --git a/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift b/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift index 92b140e11d..dc5e7c1b57 100644 --- a/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift +++ b/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift @@ -237,7 +237,7 @@ final class FeedbackViewController: NSViewController { switch selectedFormOption { case .feedback: - if !browserFeedbackTextView.string.trimmingWhitespaces().isEmpty { + if !browserFeedbackTextView.string.trimmingWhitespace().isEmpty { submitButton.isEnabled = true } else { submitButton.isEnabled = false diff --git a/DuckDuckGo/File Download/Model/FileDownloadManager.swift b/DuckDuckGo/File Download/Model/FileDownloadManager.swift index 316823b760..7b97ee10e0 100644 --- a/DuckDuckGo/File Download/Model/FileDownloadManager.swift +++ b/DuckDuckGo/File Download/Model/FileDownloadManager.swift @@ -178,7 +178,7 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { // drop known extension, it would be appended by SavePanel var suggestedFilename = suggestedFilename if let ext = fileType?.fileExtension { - suggestedFilename = suggestedFilename.drop(suffix: "." + ext) + suggestedFilename = suggestedFilename.dropping(suffix: "." + ext) } locationChooser(suggestedFilename, downloadLocation, fileType.map { [$0] } ?? []) { url, fileType in diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 845f626f39..8913b3761b 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -78,7 +78,7 @@ final class Fire { // Drop www prefixes to produce list of burning domains static func getBurningDomain(from url: URL) -> String? { - return url.host?.dropWWW() + return url.host?.droppingWwwPrefix() } private typealias TabCollectionsCleanupInfo = [TabCollectionViewModel: [TabCollectionViewModel.TabCleanupInfo]] diff --git a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift index 4838cd126d..85e088b31b 100644 --- a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift +++ b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift @@ -17,6 +17,7 @@ // import Foundation +import BrowserServicesKit private typealias URLPatterns = [String: [NSRegularExpression]] @@ -82,7 +83,7 @@ extension URL { } private func matches(any patterns: URLPatterns) -> Bool { - guard let host = self.host?.dropWWW(), + guard let host = self.host?.droppingWwwPrefix(), let matchingKey = patterns.keys.first(where: { host.contains($0) }), let pattern = patterns[matchingKey] else { return false } diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift index f7183a37c6..799124c9ca 100644 --- a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift @@ -51,7 +51,7 @@ internal class FireproofDomains { private func loadFireproofDomains() -> FireproofDomainsContainer { dispatchPrecondition(condition: .onQueue(.main)) do { - if let domains = legacyUserDefaultsFireproofDomains?.map({ $0.dropWWW() }), + if let domains = legacyUserDefaultsFireproofDomains?.map({ $0.droppingWwwPrefix() }), !domains.isEmpty { var container = FireproofDomainsContainer() @@ -91,7 +91,7 @@ internal class FireproofDomains { return } - let domainWithoutWWW = domain.dropWWW() + let domainWithoutWWW = domain.droppingWwwPrefix() do { let id = try store.add(domainWithoutWWW) try container.add(domain: domainWithoutWWW, withId: id) @@ -129,7 +129,7 @@ internal class FireproofDomains { } func isFireproof(cookieDomain: String) -> Bool { - let domainWithoutDotPrefix = cookieDomain.drop(prefix: ".") + let domainWithoutDotPrefix = cookieDomain.dropping(prefix: ".") return container.contains(domain: domainWithoutDotPrefix, includingSuperdomains: false) || (cookieDomain.hasPrefix(".") && container.contains(superdomain: domainWithoutDotPrefix)) } diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift index d9ca287dd9..94f527f2f4 100644 --- a/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift @@ -39,7 +39,7 @@ struct FireproofDomainsContainer { @discardableResult mutating func add(domain: String, withId id: NSManagedObjectID) throws -> String { - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() try domainsToIds.updateInPlace(key: domain) { value in guard value == nil else { throw DomainAlreadyAdded() } value = id @@ -57,7 +57,7 @@ struct FireproofDomainsContainer { } mutating func remove(domain: String) -> NSManagedObjectID? { - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() guard let idx = domainsToIds.index(forKey: domain) else { assertionFailure("\(domain) is not Fireproof") return nil @@ -82,7 +82,7 @@ struct FireproofDomainsContainer { } func contains(domain: String, includingSuperdomains: Bool = true) -> Bool { - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() return domainsToIds[domain] != nil || (includingSuperdomains && contains(superdomain: domain)) } diff --git a/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift b/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift index 655741fe5d..d489d8fbe6 100644 --- a/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift +++ b/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift @@ -29,7 +29,7 @@ final class FireproofInfoViewController: NSViewController { let storyboard = NSStoryboard(name: Constants.storyboardName, bundle: nil) return storyboard.instantiateController(identifier: Constants.identifier) { coder in - return FireproofInfoViewController(coder: coder, domain: domain.dropWWW()) + return FireproofInfoViewController(coder: coder, domain: domain.droppingWwwPrefix()) } } diff --git a/DuckDuckGo/History/Model/HistoryEntry.swift b/DuckDuckGo/History/Model/HistoryEntry.swift index 7efdc8ed7c..ded1f3f198 100644 --- a/DuckDuckGo/History/Model/HistoryEntry.swift +++ b/DuckDuckGo/History/Model/HistoryEntry.swift @@ -80,7 +80,7 @@ final class HistoryEntry { func addBlockedTracker(entityName: String) { numberOfTrackersBlocked += 1 - guard !entityName.trimWhitespace().isEmpty else { + guard !entityName.trimmingWhitespace().isEmpty else { return } blockedTrackingEntities.insert(entityName) diff --git a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift index 9ba31b777b..4606bd7f09 100644 --- a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift @@ -64,7 +64,7 @@ final class RecentlyVisitedModel: ObservableObject { .forEach { numberOfTrackersBlocked += $0.numberOfTrackersBlocked - guard let host = $0.url.host?.dropWWW() else { return } + guard let host = $0.url.host?.droppingWwwPrefix() else { return } var site = sitesByDomain[host] if site == nil { @@ -100,7 +100,7 @@ final class RecentlyVisitedModel: ObservableObject { bookmarkManager.update(bookmark: bookmark) site.isFavorite = bookmark.isFavorite } else { - bookmarkManager.makeBookmark(for: url, title: site.domain.dropWWW(), isFavorite: true) + bookmarkManager.makeBookmark(for: url, title: site.domain.droppingWwwPrefix(), isFavorite: true) site.isFavorite = true } } @@ -206,9 +206,9 @@ final class RecentlyVisitedSiteModel: ObservableObject { } else if !showTitlesForPagesSetting { $0.displayTitle = $0.url.absoluteString - .drop(prefix: "https://") - .drop(prefix: "http://") - .drop(prefix: $0.url.host ?? "") + .dropping(prefix: "https://") + .dropping(prefix: "http://") + .dropping(prefix: $0.url.host ?? "") } else if $0.actualTitle?.isEmpty ?? true { // Blank titles diff --git a/DuckDuckGo/Home Page/View/FavoritesView.swift b/DuckDuckGo/Home Page/View/FavoritesView.swift index 8754975d66..f79a869a94 100644 --- a/DuckDuckGo/Home Page/View/FavoritesView.swift +++ b/DuckDuckGo/Home Page/View/FavoritesView.swift @@ -32,58 +32,103 @@ struct Favorites: View { var body: some View { - let addButton = ZStack(alignment: .top) { - FavoriteTemplate(title: UserText.addFavorite, domain: nil) - ZStack { - Image("Add") - .resizable() - .frame(width: 22, height: 22) - }.frame(width: 64, height: 64) - } - .link { - model.addNew() + if #available(macOS 11.0, *) { + LazyVStack(spacing: 4) { + FavoritesGrid(isHovering: $isHovering) + } + .frame(maxWidth: .infinity) + .onHover { isHovering in + self.isHovering = isHovering + } + } else { + VStack(spacing: 4) { + FavoritesGrid(isHovering: $isHovering) + } + .frame(maxWidth: .infinity) + .onHover { isHovering in + self.isHovering = isHovering + } } - let ghostButton = VStack { - RoundedRectangle(cornerRadius: 12) - .stroke(Color("HomeFavoritesGhostColor"), style: StrokeStyle(lineWidth: 1.5, dash: [4.0, 2.0])) - .frame(width: 64, height: 64) - }.frame(width: 64) + } - VStack(spacing: 4) { +} + +struct FavoritesGrid: View { + + @EnvironmentObject var model: HomePage.Models.FavoritesModel + + @Binding var isHovering: Bool + + var rowIndices: Range { + model.showAllFavorites ? model.rows.indices : model.rows.indices.prefix(HomePage.favoritesRowCountWhenCollapsed) + } + + var body: some View { - ForEach(rowIndices, id: \.self) { index in + ForEach(rowIndices, id: \.self) { index in - HStack(alignment: .top, spacing: 20) { - ForEach(model.rows[index], id: \.id) { favorite in + HStack(alignment: .top, spacing: 20) { + ForEach(model.rows[index], id: \.id) { favorite in - switch favorite.favoriteType { - case .bookmark(let bookmark): - Favorite(bookmark: bookmark) + switch favorite.favoriteType { + case .bookmark(let bookmark): + Favorite(bookmark: bookmark) - case .addButton: - addButton + case .addButton: + FavoritesGridAddButton() - case .ghostButton: - ghostButton - } + case .ghostButton: + FavoritesGridGhostButton() } } - } + + } - MoreOrLess(isExpanded: $model.showAllFavorites) - .padding(.top, 2) - .visibility(model.rows.count > HomePage.favoritesRowCountWhenCollapsed && isHovering ? .visible : .invisible) + MoreOrLess(isExpanded: $model.showAllFavorites) + .padding(.top, 2) + .visibility(model.rows.count > HomePage.favoritesRowCountWhenCollapsed && isHovering ? .visible : .invisible) + } + +} + +private struct FavoritesGridAddButton: View { + + @EnvironmentObject var model: HomePage.Models.FavoritesModel + + var body: some View { + + ZStack(alignment: .top) { + FavoriteTemplate(title: UserText.addFavorite, domain: nil) + ZStack { + Image("Add") + .resizable() + .frame(width: 22, height: 22) + }.frame(width: 64, height: 64) } - .frame(maxWidth: .infinity) - .onHover { isHovering in - self.isHovering = isHovering + .link { + model.addNew() } - + } - + +} + +private struct FavoritesGridGhostButton: View { + + var body: some View { + + VStack { + RoundedRectangle(cornerRadius: 12) + .stroke(Color("HomeFavoritesGhostColor"), style: StrokeStyle(lineWidth: 1.5, dash: [4.0, 2.0])) + .frame(width: 64, height: 64) + } + .frame(width: 64) + + } + } struct FavoriteTemplate: View { diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 22831a7b6c..9c82d6aaab 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -123,10 +123,31 @@ final class MainMenu: NSMenu { #endif subscribeToBookmarkList() + subscribeToFavicons() } // MARK: - Bookmarks + var faviconsCancellable: AnyCancellable? + private func subscribeToFavicons() { + faviconsCancellable = FaviconManager.shared.$faviconsLoaded + .receive(on: DispatchQueue.main).sink(receiveValue: { [weak self] loaded in + if loaded { + self?.updateFavicons(self?.bookmarksMenuItem) + self?.updateFavicons(self?.favoritesMenuItem) + } + }) + } + + private func updateFavicons(_ menuItem: NSMenuItem?) { + if let bookmark = menuItem?.representedObject as? Bookmark { + menuItem?.image = BookmarkViewModel(entity: bookmark).menuFavicon + } + menuItem?.submenu?.items.forEach { menuItem in + updateFavicons(menuItem) + } + } + var bookmarkListCancellable: AnyCancellable? private func subscribeToBookmarkList() { bookmarkListCancellable = LocalBookmarkManager.shared.$list diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift index c1d99f79f2..9f2b945963 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift @@ -265,11 +265,11 @@ final class AddressBarTextField: NSTextField { let oldURL = selectedTabViewModel.tab.content.url, oldURL.isDuckDuckGoSearch { if let ia = try? oldURL.getParameter(name: URL.DuckDuckGoParameters.ia.rawValue), - let newURL = try? url.addParameter(name: URL.DuckDuckGoParameters.ia.rawValue, value: ia) { + let newURL = try? url.appendingParameter(name: URL.DuckDuckGoParameters.ia.rawValue, value: ia) { url = newURL } if let iax = try? oldURL.getParameter(name: URL.DuckDuckGoParameters.iax.rawValue), - let newURL = try? url.addParameter(name: URL.DuckDuckGoParameters.iax.rawValue, value: iax) { + let newURL = try? url.appendingParameter(name: URL.DuckDuckGoParameters.iax.rawValue, value: iax) { url = newURL } } @@ -531,7 +531,7 @@ final class AddressBarTextField: NSTextField { private var stringValueWithoutSuffix: String { if let suffix = suffix { - return stringValue.drop(suffix: suffix.string) + return stringValue.dropping(suffix: suffix.string) } else { return stringValue } @@ -670,7 +670,7 @@ final class AddressBarTextField: NSTextField { @objc private func pasteAndGo(_ menuItem: NSMenuItem) { guard let pasteboardString = NSPasteboard.general.string(forType: .string), - let url = URL(trimmedAddressBarString: pasteboardString.trimmingWhitespaces()) else { + let url = URL(trimmedAddressBarString: pasteboardString.trimmingWhitespace()) else { assertionFailure("Pasteboard doesn't contain URL") return } @@ -977,7 +977,7 @@ extension AddressBarTextField: NSTextViewDelegate { } private func makePasteAndDoMenuItem() -> NSMenuItem? { - if let trimmedPasteboardString = NSPasteboard.general.string(forType: .string)?.trimmingWhitespaces(), + if let trimmedPasteboardString = NSPasteboard.general.string(forType: .string)?.trimmingWhitespace(), trimmedPasteboardString.count > 0 { if URL(trimmedAddressBarString: trimmedPasteboardString) != nil { return Self.pasteAndGoMenuItem diff --git a/DuckDuckGo/Permissions/Model/PermissionManager.swift b/DuckDuckGo/Permissions/Model/PermissionManager.swift index d215e7a0cf..c93cb211dc 100644 --- a/DuckDuckGo/Permissions/Model/PermissionManager.swift +++ b/DuckDuckGo/Permissions/Model/PermissionManager.swift @@ -52,7 +52,7 @@ final class PermissionManager: PermissionManagerProtocol { do { let entities = try store.loadPermissions() for entity in entities { - self.set(entity.permission, forDomain: entity.domain.dropWWW(), permissionType: entity.type) + self.set(entity.permission, forDomain: entity.domain.droppingWwwPrefix(), permissionType: entity.type) } } catch { os_log("PermissionStore: Failed to load permissions", type: .error) @@ -67,11 +67,11 @@ final class PermissionManager: PermissionManagerProtocol { private(set) var persistedPermissionTypes = Set() func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision { - return permissions[domain.dropWWW()]?[permissionType]?.decision ?? .ask + return permissions[domain.droppingWwwPrefix()]?[permissionType]?.decision ?? .ask } func hasPermissionPersisted(forDomain domain: String, permissionType: PermissionType) -> Bool { - return permissions[domain.dropWWW()]?[permissionType] != nil + return permissions[domain.droppingWwwPrefix()]?[permissionType] != nil } func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) { @@ -79,7 +79,7 @@ final class PermissionManager: PermissionManagerProtocol { assert(permissionType.canPersistDeniedDecision || decision != .deny) let storedPermission: StoredPermission - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() guard self.permission(forDomain: domain, permissionType: permissionType) != decision else { return } defer { diff --git a/DuckDuckGo/Permissions/Model/PermissionModel.swift b/DuckDuckGo/Permissions/Model/PermissionModel.swift index 358bea7e6f..4760dd654a 100644 --- a/DuckDuckGo/Permissions/Model/PermissionModel.swift +++ b/DuckDuckGo/Permissions/Model/PermissionModel.swift @@ -198,7 +198,7 @@ final class PermissionModel { to decision: PersistedPermissionDecision) { // If Always Allow/Deny for the current host: Grant/Revoke the permission - guard webView?.url?.host?.dropWWW() == domain else { return } + guard webView?.url?.host?.droppingWwwPrefix() == domain else { return } switch (decision, self.permissions[permissionType]) { case (.deny, .some): diff --git a/DuckDuckGo/Permissions/Model/PermissionType.swift b/DuckDuckGo/Permissions/Model/PermissionType.swift index 39b2a69fdd..8f4c2f7ab0 100644 --- a/DuckDuckGo/Permissions/Model/PermissionType.swift +++ b/DuckDuckGo/Permissions/Model/PermissionType.swift @@ -52,7 +52,7 @@ enum PermissionType: Hashable { case Constants.popups.rawValue: self = .popups default: if rawValue.hasPrefix(Constants.external.rawValue) { - let scheme = rawValue.drop(prefix: Constants.external.rawValue) + let scheme = rawValue.dropping(prefix: Constants.external.rawValue) guard !scheme.isEmpty else { return nil } self = .externalScheme(scheme: scheme) return diff --git a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift index 4147f58fb6..c924d827a3 100644 --- a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift +++ b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift @@ -42,7 +42,7 @@ final class PermissionContextMenu: NSMenu { init(permissions: [(key: PermissionType, value: PermissionState)], domain: String, delegate: PermissionContextMenuDelegate?) { - self.domain = domain.dropWWW() + self.domain = domain.droppingWwwPrefix() self.permissions = permissions self.actionDelegate = delegate super.init(title: "") diff --git a/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift b/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift index 230101ee0a..70793782e0 100644 --- a/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift +++ b/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift @@ -67,7 +67,7 @@ final class PinnedTabsManager { } var pinnedDomains: Set { - Set(tabCollection.tabs.compactMap { $0.url?.host?.dropWWW() }) + Set(tabCollection.tabs.compactMap { $0.url?.host?.droppingWwwPrefix() }) } func setUp(with collection: TabCollection) { diff --git a/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift b/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift index 988754b54b..6abe1aa858 100644 --- a/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift +++ b/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift @@ -121,10 +121,10 @@ struct PinnedTabInnerView: View { if let favicon = model.favicon { Image(nsImage: favicon) .resizable() - } else if let domain = model.content.url?.host, let firstLetter = domain.dropWWW().capitalized.first.flatMap(String.init) { + } else if let domain = model.content.url?.host, let firstLetter = domain.droppingWwwPrefix().capitalized.first.flatMap(String.init) { ZStack { Rectangle() - .foregroundColor(.forDomain(domain.dropWWW())) + .foregroundColor(.forDomain(domain.droppingWwwPrefix())) Text(firstLetter) .font(.caption) .foregroundColor(.white) diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index 92f87813b5..5e59df69e3 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -48,7 +48,7 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { init(bundleIdentifier: String = AppVersion.shared.identifier) { var bundleID = bundleIdentifier #if DEBUG - bundleID = bundleID.drop(suffix: ".debug") + bundleID = bundleID.dropping(suffix: ".debug") #endif self.bundleIdentifier = bundleIdentifier } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index daeb7d89aa..b8c9197a53 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -54,7 +54,7 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { init?(url: URL) { // manually extract path because URLs such as "about:preferences" can't figure out their host or path - let path = url.absoluteString.drop(prefix: URL.preferences.absoluteString + "/") + let path = url.absoluteString.dropping(prefix: URL.preferences.absoluteString + "/") self.init(rawValue: path) } diff --git a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift index afa1797804..76b88b885e 100644 --- a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift +++ b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift @@ -98,7 +98,7 @@ extension FireproofDomainsViewController: NSTableViewDataSource, NSTableViewDele func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if let cell = tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil) as? NSTableCellView { let domain = fireproofDomains[row] - cell.textField?.stringValue = domain.dropWWW() + cell.textField?.stringValue = domain.droppingWwwPrefix() cell.imageView?.image = faviconManagement.getCachedFavicon(for: domain, sizeCategory: .small)?.image cell.imageView?.applyFaviconStyle() @@ -122,7 +122,7 @@ extension FireproofDomainsViewController: NSTextFieldDelegate { if field.stringValue.isEmpty { filteredFireproofDomains = nil } else { - filteredFireproofDomains = allFireproofDomains.filter { $0.dropWWW().contains(field.stringValue) } + filteredFireproofDomains = allFireproofDomains.filter { $0.droppingWwwPrefix().contains(field.stringValue) } } reloadData() diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift index 040dfda212..0ec658e6e6 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift @@ -112,7 +112,7 @@ enum SecureVaultItem: Equatable, Identifiable, Comparable { var displayTitle: String { switch self { case .account(let account): - return ((account.title ?? "").isEmpty == true ? account.domain.dropWWW() : account.title) ?? "" + return ((account.title ?? "").isEmpty == true ? account.domain.droppingWwwPrefix() : account.title) ?? "" case .card(let card): return card.title case .identity(let identity): diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift index afa6b83b4c..5b075ffab7 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift @@ -98,13 +98,13 @@ final class PasswordManagementLoginModel: ObservableObject, PasswordManagementIt } func normalizedDomain(_ domain: String) -> String { - let trimmed = domain.trimmingWhitespaces() + let trimmed = domain.trimmingWhitespace() if !trimmed.starts(with: "https://") && !trimmed.starts(with: "http://") && trimmed.contains("://") { // Contains some other protocol, so don't mess with it return domain } - let noSchemeOrWWW = domain.drop(prefix: "https://").drop(prefix: "http://").dropWWW() + let noSchemeOrWWW = domain.dropping(prefix: "https://").dropping(prefix: "http://").droppingWwwPrefix() return URLComponents(string: "https://\(noSchemeOrWWW)")?.host ?? "" } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift index 91c9ae291d..50763b3423 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift @@ -332,12 +332,12 @@ private struct HeaderView: View { if model.isNew || model.isEditing { - TextField(model.domain.dropWWW(), text: $model.title) + TextField(model.domain.droppingWwwPrefix(), text: $model.title) .font(.title) } else { - Text(model.title.isEmpty ? model.domain.dropWWW() : model.title) + Text(model.title.isEmpty ? model.domain.droppingWwwPrefix() : model.title) .font(.title) } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift index 726b8cffb0..5f150d56cd 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift @@ -208,7 +208,7 @@ final class PasswordManagementViewController: NSViewController { // Only select the matching item directly if macOS 11 is available, as 10.15 doesn't support scrolling directly to a given // item in SwiftUI. On 10.15, show the matching item by filtering the search bar automatically instead. if #available(macOS 11.0, *) { - refetchWithText("", selectItemMatchingDomain: domain?.dropWWW(), clearWhenNoMatches: true) + refetchWithText("", selectItemMatchingDomain: domain?.droppingWwwPrefix(), clearWhenNoMatches: true) } else { refetchWithText(isDirty ? "" : domain ?? "", clearWhenNoMatches: true) } @@ -692,7 +692,7 @@ final class PasswordManagementViewController: NSViewController { } private func updateFilter() { - let text = searchField.stringValue.trimmingWhitespaces() + let text = searchField.stringValue.trimmingWhitespace() listModel?.filter = text } diff --git a/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift b/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift index c143345389..6b0c71d5e0 100644 --- a/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift @@ -120,7 +120,7 @@ final class SaveCredentialsViewController: NSViewController { self.delegate?.shouldCloseSaveCredentialsViewController(self) } - var account = SecureVaultModels.WebsiteAccount(username: usernameField.stringValue.trimmingWhitespaces(), + var account = SecureVaultModels.WebsiteAccount(username: usernameField.stringValue.trimmingWhitespace(), domain: domainLabel.stringValue) account.id = credentials?.account.id let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) @@ -169,7 +169,7 @@ final class SaveCredentialsViewController: NSViewController { return } - let alert = NSAlert.fireproofAlert(with: host.dropWWW()) + let alert = NSAlert.fireproofAlert(with: host.droppingWwwPrefix()) alert.beginSheetModal(for: window) { response in if response == NSApplication.ModalResponse.alertFirstButtonReturn { Pixel.fire(.fireproof(kind: .pwm, suggested: .suggested)) diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 5bec9fc19a..d10635e34b 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -99,9 +99,9 @@ extension SuggestionContainer: SuggestionLoadingDataSource { completion: @escaping (Data?, Error?) -> Void) { var url = url parameters.forEach { - if let newUrl = try? url.addParameter(name: $0.key, value: $0.value) { - url = newUrl - } else { + do { + try url = url.appendingParameter(name: $0.key, value: $0.value) + } catch { assertionFailure("SuggestionContainer: Failed to add parameter") } } diff --git a/DuckDuckGo/User Agent/Model/UserAgent.swift b/DuckDuckGo/User Agent/Model/UserAgent.swift index 9ff4234d80..06373b0951 100644 --- a/DuckDuckGo/User Agent/Model/UserAgent.swift +++ b/DuckDuckGo/User Agent/Model/UserAgent.swift @@ -17,6 +17,7 @@ // import Foundation +import BrowserServicesKit enum UserAgent { diff --git a/Unit Tests/Common/Extensions/URLExtensionTests.swift b/Unit Tests/Common/Extensions/URLExtensionTests.swift index a1080b241b..c503a01320 100644 --- a/Unit Tests/Common/Extensions/URLExtensionTests.swift +++ b/Unit Tests/Common/Extensions/URLExtensionTests.swift @@ -22,26 +22,6 @@ import Combine final class URLExtensionTests: XCTestCase { - func test_external_urls_are_valid() { - XCTAssertTrue("mailto://user@host.tld".url!.isValid) - XCTAssertTrue("sms://+44776424232323".url!.isValid) - XCTAssertTrue("ftp://example.com".url!.isValid) - } - - func test_navigational_urls_are_valid() { - XCTAssertTrue("http://example.com".url!.isValid) - XCTAssertTrue("https://example.com".url!.isValid) - XCTAssertTrue("http://localhost".url!.isValid) - XCTAssertTrue("http://localdomain".url!.isValid) - } - - func test_when_no_scheme_in_string_url_has_scheme() { - XCTAssertEqual("duckduckgo.com".url!.absoluteString, "http://duckduckgo.com") - XCTAssertEqual("example.com".url!.absoluteString, "http://example.com") - XCTAssertEqual("localhost".url!.absoluteString, "http://localhost") - XCTAssertNil("localdomain".url) - } - func test_makeURL_from_addressBarString() { let data: [(string: String, expected: String)] = [ ("https://duckduckgo.com/?q=search string with spaces", "https://duckduckgo.com/?q=search%20string%20with%20spaces"), @@ -103,81 +83,4 @@ final class URLExtensionTests: XCTestCase { } } - func testWhenPunycodeUrlIsCalledOnEmptyStringThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: "")?.absoluteString) - } - - func testWhenPunycodeUrlIsCalledOnQueryThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: " ")?.absoluteString) - } - - func testWhenPunycodeUrlIsCalledOnQueryWithSpaceThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: "https://www.duckduckgo .com/html?q=search")?.absoluteString) - XCTAssertNil(URL(trimmedAddressBarString: "https://www.duckduckgo.com/html?q =search")?.absoluteString) - } - - func testWhenPunycodeUrlIsCalledOnLocalHostnameThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: "πŸ’©")?.absoluteString) - } - - func testWhenDefineSearchRequestIsMadeItIsNotInterpretedAsLocalURL() { - XCTAssertNil(URL(trimmedAddressBarString: "define:300/spartans")?.absoluteString) - } - - func testAddressBarURLParsing() { - let addresses = [ - "user@somehost.local:9091/index.html", - "something.local:9100", - "user@localhost:5000", - "user:password@localhost:5000", - "localhost", - "localhost:5000", - "sms://+44123123123", - "mailto:test@example.com", - "https://", - "http://duckduckgo.com", - "https://duckduckgo.com", - "https://duckduckgo.com/", - "duckduckgo.com", - "duckduckgo.com/html?q=search", - "www.duckduckgo.com", - "https://www.duckduckgo.com/html?q=search", - "https://www.duckduckgo.com/html/?q=search", - "ftp://www.duckduckgo.com", - "file:///users/user/Documents/afile" - ] - - for address in addresses { - let url = URL(trimmedAddressBarString: address) - var expectedString = address - let expectedScheme = address.split(separator: "/").first.flatMap { - $0.hasSuffix(":") ? String($0).drop(suffix: ":") : nil - }?.lowercased() ?? "http" - if !address.hasPrefix(expectedScheme) { - expectedString = expectedScheme + "://" + address - } - XCTAssertEqual(url?.scheme, expectedScheme) - XCTAssertEqual(url?.absoluteString, expectedString) - } - } - - func testWhenPunycodeUrlIsCalledWithEncodedUrlsThenUrlIsReturned() { - XCTAssertEqual("http://xn--ls8h.la", "πŸ’©.la".decodedURL?.absoluteString) - XCTAssertEqual("http://xn--ls8h.la/", "πŸ’©.la/".decodedURL?.absoluteString) - XCTAssertEqual("http://82.xn--b1aew.xn--p1ai", "82.ΠΌΠ²Π΄.Ρ€Ρ„".decodedURL?.absoluteString) - XCTAssertEqual("http://xn--ls8h.la:8080", "http://πŸ’©.la:8080".decodedURL?.absoluteString) - XCTAssertEqual("http://xn--ls8h.la", "http://πŸ’©.la".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la", "https://πŸ’©.la".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/", "https://πŸ’©.la/".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/path/to/resource", "https://πŸ’©.la/path/to/resource".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/path/to/resource?query=true", "https://πŸ’©.la/path/to/resource?query=true".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/%F0%9F%92%A9", "https://πŸ’©.la/πŸ’©".decodedURL?.absoluteString) - } - -} - -private extension String { - var decodedURL: URL? { - URL(trimmedAddressBarString: self) - } } diff --git a/Unit Tests/Permissions/PermissionManagerMock.swift b/Unit Tests/Permissions/PermissionManagerMock.swift index ae7be64654..22f273d92d 100644 --- a/Unit Tests/Permissions/PermissionManagerMock.swift +++ b/Unit Tests/Permissions/PermissionManagerMock.swift @@ -30,16 +30,16 @@ final class PermissionManagerMock: PermissionManagerProtocol { var savedPermissions = [String: [PermissionType: Bool]]() func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision { - guard let allow = savedPermissions[domain.dropWWW()]?[permissionType] else { return .ask } + guard let allow = savedPermissions[domain.droppingWwwPrefix()]?[permissionType] else { return .ask } return PersistedPermissionDecision(allow: allow, isRemoved: false) } func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) { - savedPermissions[domain.dropWWW(), default: [:]][permissionType] = decision == .ask ? nil : decision.boolValue + savedPermissions[domain.droppingWwwPrefix(), default: [:]][permissionType] = decision == .ask ? nil : decision.boolValue } func removePermission(forDomain domain: String, permissionType: PermissionType) { - savedPermissions[domain.dropWWW(), default: [:]][permissionType] = nil + savedPermissions[domain.droppingWwwPrefix(), default: [:]][permissionType] = nil } var burnPermissionsCalled = false diff --git a/Unit Tests/Permissions/PermissionManagerTests.swift b/Unit Tests/Permissions/PermissionManagerTests.swift index e48d68dd39..da1f751a90 100644 --- a/Unit Tests/Permissions/PermissionManagerTests.swift +++ b/Unit Tests/Permissions/PermissionManagerTests.swift @@ -34,7 +34,7 @@ final class PermissionManagerTests: XCTestCase { store.permissions = [.entity1, .entity2] let result1 = manager.permission(forDomain: "www." + PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) - let result2 = manager.permission(forDomain: PermissionEntity.entity2.domain.dropWWW(), + let result2 = manager.permission(forDomain: PermissionEntity.entity2.domain.droppingWwwPrefix(), permissionType: PermissionEntity.entity2.type) let result3 = manager.permission(forDomain: "otherdomain.com", permissionType: .microphone) @@ -115,7 +115,7 @@ final class PermissionManagerTests: XCTestCase { .add(domain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type, decision: .allow), - .add(domain: PermissionEntity.entity2.domain.dropWWW(), + .add(domain: PermissionEntity.entity2.domain.droppingWwwPrefix(), permissionType: PermissionEntity.entity2.type, decision: .deny)]) XCTAssertEqual(result1, .allow) @@ -182,7 +182,7 @@ final class PermissionManagerTests: XCTestCase { let e = expectation(description: "permission published") let c = manager.permissionPublisher.sink { value in - XCTAssertEqual(value.domain, PermissionEntity.entity2.domain.dropWWW()) + XCTAssertEqual(value.domain, PermissionEntity.entity2.domain.droppingWwwPrefix()) XCTAssertEqual(value.permissionType, PermissionEntity.entity2.type) XCTAssertEqual(value.decision, .ask) e.fulfill() @@ -218,7 +218,7 @@ final class PermissionManagerTests: XCTestCase { let fireproofDomains = FireproofDomains(store: FireproofDomainsStoreMock()) fireproofDomains.add(domain: PermissionEntity.entity1.domain) - manager.burnPermissions(of: [PermissionEntity.entity2.domain.dropWWW()]) {} + manager.burnPermissions(of: [PermissionEntity.entity2.domain.droppingWwwPrefix()]) {} XCTAssertEqual(store.history, [.load, .clear(exceptions: [PermissionEntity.entity1.permission])]) XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity1.domain,