diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index 6f32c69292..755fffc129 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -92,4 +92,20 @@ extension String { return hasPrefix(other) || other.hasPrefix(self) } + // MARK: - Search with bang + + private static let searchWithBangPattern = "^.+\\?q=%21[a-zA-Z]+(?:%20[a-zA-Z]+)*$" + + private static var compiledSearchWithBangRegex: NSRegularExpression? = { + if let newRegex = try? NSRegularExpression(pattern: searchWithBangPattern, options: .caseInsensitive) { + return newRegex + } + return nil + }() + + var isSearchWithBang: Bool { + guard let regex = Self.compiledSearchWithBangRegex else { return false } + return self.url?.isDuckDuckGoSearch ?? false && regex.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) != nil + } + } diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift index 55b81c2954..dc3686ef28 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift @@ -134,6 +134,13 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { } } + // Remove duckduckgo search with bang + for (index, item) in list.enumerated().reversed() { + if let url = item.url, url.absoluteString.isSearchWithBang { + list.remove(at: index) + } + } + return (list, currentIndex) } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 898216a0bd..159c798ac2 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -670,7 +670,8 @@ protocol NewWindowPolicyDecisionMaker { return } - let canGoBack = webView.canGoBack || self.error != nil + let isJustOneRidirectGoingBack = webView.backForwardList.backList.count == 1 && webView.backForwardList.backItem?.url.absoluteString.contains("%21") ?? false + let canGoBack = (webView.canGoBack && !isJustOneRidirectGoingBack) || self.error != nil let canGoForward = webView.canGoForward && self.error == nil let canReload = (self.content.urlForWebView?.scheme ?? URL.NavigationalScheme.about.rawValue) != URL.NavigationalScheme.about.rawValue @@ -700,6 +701,14 @@ protocol NewWindowPolicyDecisionMaker { } userInteractionDialog = nil + + // If search with bang go back of 2 spaces (to avoid redirect) + if let urlString = webView.backForwardList.backItem?.url.absoluteString, urlString.isSearchWithBang { + if let item = webView.backForwardList.item(at: -2) { + return webView.navigator()?.go(to: item) + } + } + return webView.navigator()?.goBack(withExpectedNavigationType: .backForward(distance: -1)) } @@ -707,6 +716,14 @@ protocol NewWindowPolicyDecisionMaker { @discardableResult func goForward() -> ExpectedNavigation? { guard canGoForward else { return nil } + + // If search with bang go forward of 2 spaces (to avoid redirect) + if let urlString = webView.backForwardList.forwardItem?.url.absoluteString, urlString.isSearchWithBang { + if let item = webView.backForwardList.item(at: +2) { + return webView.navigator()?.go(to: item) + } + } + return webView.navigator()?.goForward(withExpectedNavigationType: .backForward(distance: 1)) } diff --git a/UnitTests/Tab/TabStringExtensionTests.swift b/UnitTests/Tab/TabStringExtensionTests.swift new file mode 100644 index 0000000000..f4bfb4d55f --- /dev/null +++ b/UnitTests/Tab/TabStringExtensionTests.swift @@ -0,0 +1,46 @@ +// +// TabUrlExtensionTests.swift +// +// Copyright © 2023 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 TabStringExtensionTests: XCTestCase { + + func testSearchWithBangDetection() { + let searchWithBang: [String] = [ + "https://duckduckgo.com/?q=%21Hello", + "https://duckduckgo.com/?q=%21Hello%20World", + "https://duckduckgo.com/?q=%21Search%20With%20Bang" + ] + + let nonSearchWithBang: [String] = [ + "https://duckduckgo.com/?q=%21", + "https://duckduckgo.com/?q=%21%20", + "https://duckduckgo.com/?q=%21%20test", + "https://duckduckgo.com/?q=test%21test", + ] + + for url in searchWithBang { + XCTAssertTrue(url.isSearchWithBang, "\(url) should be detected as a search with bang") + } + + for url in nonSearchWithBang { + XCTAssertFalse(url.isSearchWithBang, "\(url) should not be detected as search with bang") + } + } +}