diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift
index 199f4b049..b2bc644b5 100644
--- a/Client/Application/Delegates/AppDelegate.swift
+++ b/Client/Application/Delegates/AppDelegate.swift
@@ -242,7 +242,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati
if isFirstLaunch {
FavoritesHelper.addDefaultFavorites()
- profile?.searchEngines.setupDefaultRegionalSearchEngines()
+ profile?.searchEngines.regionalSearchEngineSetup()
}
if let urp = UserReferralProgram.shared {
if isFirstLaunch {
diff --git a/Client/Assets/SearchPlugins/duckduckgo.xml b/Client/Assets/SearchPlugins/duckduckgo.xml
index 9e96ae21a..8b7dd106d 100644
--- a/Client/Assets/SearchPlugins/duckduckgo.xml
+++ b/Client/Assets/SearchPlugins/duckduckgo.xml
@@ -13,11 +13,11 @@
-
+
-
+
https://duckduckgo.com
diff --git a/Client/Frontend/Browser/Search/OpenSearch.swift b/Client/Frontend/Browser/Search/OpenSearch.swift
index 494798502..aa1775de2 100644
--- a/Client/Frontend/Browser/Search/OpenSearch.swift
+++ b/Client/Frontend/Browser/Search/OpenSearch.swift
@@ -4,6 +4,7 @@
import UIKit
import Shared
+import BraveShared
import Fuzi
private let TypeSearch = "text/html"
@@ -17,6 +18,8 @@ class OpenSearchEngine: NSObject, NSSecureCoding {
static let qwant = "Qwant"
}
+ static let defaultSearchClientName = "brave"
+
let shortName: String
let engineID: String?
let image: UIImage
@@ -26,10 +29,12 @@ class OpenSearchEngine: NSObject, NSSecureCoding {
fileprivate let SearchTermComponent = "{searchTerms}"
fileprivate let LocaleTermComponent = "{moz:locale}"
+ fileprivate let RegionalClientComponent = "{customClient}"
fileprivate lazy var searchQueryComponentKey: String? = self.getQueryArgFromTemplate()
- init(engineID: String?, shortName: String, image: UIImage, searchTemplate: String, suggestTemplate: String?, isCustomEngine: Bool) {
+ init(engineID: String?, shortName: String, image: UIImage, searchTemplate: String,
+ suggestTemplate: String?, isCustomEngine: Bool) {
self.shortName = shortName
self.image = image
self.searchTemplate = searchTemplate
@@ -73,8 +78,8 @@ class OpenSearchEngine: NSObject, NSSecureCoding {
/**
* Returns the search URL for the given query.
*/
- func searchURLForQuery(_ query: String) -> URL? {
- return getURLFromTemplate(searchTemplate, query: query)
+ func searchURLForQuery(_ query: String, locale: Locale = Locale.current) -> URL? {
+ return getURLFromTemplate(searchTemplate, query: query, locale: locale)
}
/**
@@ -86,7 +91,9 @@ class OpenSearchEngine: NSObject, NSSecureCoding {
// a valid URL, otherwise we cannot do the conversion to NSURLComponents
// and have to do flaky pattern matching instead.
let placeholder = "PLACEHOLDER"
- let template = searchTemplate.replacingOccurrences(of: SearchTermComponent, with: placeholder)
+ let template = searchTemplate
+ .replacingOccurrences(of: SearchTermComponent, with: placeholder)
+ .replacingOccurrences(of: RegionalClientComponent, with: placeholder)
let components = URLComponents(string: template)
let searchTerm = components?.queryItems?.filter { item in
return item.value == placeholder
@@ -121,32 +128,47 @@ class OpenSearchEngine: NSObject, NSSecureCoding {
/**
* Returns the search suggestion URL for the given query.
*/
- func suggestURLForQuery(_ query: String) -> URL? {
+ func suggestURLForQuery(_ query: String, locale: Locale = Locale.current) -> URL? {
if let suggestTemplate = suggestTemplate {
- return getURLFromTemplate(suggestTemplate, query: query)
+ return getURLFromTemplate(suggestTemplate, query: query, locale: locale)
}
return nil
}
- fileprivate func getURLFromTemplate(_ searchTemplate: String, query: String) -> URL? {
- if let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: .SearchTermsAllowed) {
- // Escape the search template as well in case it contains not-safe characters like symbols
- let templateAllowedSet = NSMutableCharacterSet()
- templateAllowedSet.formUnion(with: .URLAllowed)
-
- // Allow brackets since we use them in our template as our insertion point
- templateAllowedSet.formUnion(with: CharacterSet(charactersIn: "{}"))
-
- if let encodedSearchTemplate = searchTemplate.addingPercentEncoding(withAllowedCharacters: templateAllowedSet as CharacterSet) {
- let localeString = Locale.current.identifier
- let urlString = encodedSearchTemplate
- .replacingOccurrences(of: SearchTermComponent, with: escapedQuery, options: .literal, range: nil)
- .replacingOccurrences(of: LocaleTermComponent, with: localeString, options: .literal, range: nil)
- return URL(string: urlString)
+ fileprivate func getURLFromTemplate(_ searchTemplate: String, query: String, locale: Locale) -> URL? {
+ guard let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: .SearchTermsAllowed) else {
+ return nil
+ }
+
+ // Escape the search template as well in case it contains not-safe characters like symbols
+ let templateAllowedSet = NSMutableCharacterSet()
+ templateAllowedSet.formUnion(with: .URLAllowed)
+
+ // Allow brackets since we use them in our template as our insertion point
+ templateAllowedSet.formUnion(with: CharacterSet(charactersIn: "{}"))
+
+ guard let encodedSearchTemplate = searchTemplate.addingPercentEncoding(withAllowedCharacters:
+ templateAllowedSet as CharacterSet) else { return nil }
+
+ let localeString = locale.identifier
+ let urlString = encodedSearchTemplate
+ .replacingOccurrences(of: SearchTermComponent, with: escapedQuery, options: .literal, range: nil)
+ .replacingOccurrences(of: LocaleTermComponent, with: localeString, options: .literal, range: nil)
+ .replacingOccurrences(of: RegionalClientComponent, with: regionalClientParam(locale),
+ options: .literal, range: nil)
+ return URL(string: urlString)
+ }
+
+ private func regionalClientParam(_ locale: Locale) -> String {
+ if shortName == EngineNames.duckDuckGo, let region = locale.regionCode {
+ switch region {
+ case "AU", "IE", "NZ": return "braveed"
+ case "DE": return "bravened"
+ default: break
}
}
-
- return nil
+
+ return OpenSearchEngine.defaultSearchClientName
}
}
diff --git a/Client/Frontend/Browser/Search/SearchEngines.swift b/Client/Frontend/Browser/Search/SearchEngines.swift
index f63eff9c4..7674bec90 100644
--- a/Client/Frontend/Browser/Search/SearchEngines.swift
+++ b/Client/Frontend/Browser/Search/SearchEngines.swift
@@ -62,9 +62,14 @@ class SearchEngines {
self.orderedEngines = getOrderedEngines()
}
- func setupDefaultRegionalSearchEngines() {
- guard let region = Locale.current.regionCode,
- let searchEngine = SearchEngines.defaultRegionSearchEngines[region] else { return }
+ func regionalSearchEngineSetup(for locale: Locale = Locale.current) {
+ guard let region = locale.regionCode else { return }
+
+ setupDefaultRegionalSearchEngines(region: region)
+ }
+
+ private func setupDefaultRegionalSearchEngines(region: String) {
+ guard let searchEngine = SearchEngines.defaultRegionSearchEngines[region] else { return }
setDefaultEngine(searchEngine, forType: .standard)
setDefaultEngine(searchEngine, forType: .privateMode)
diff --git a/ClientTests/SearchEnginesTests.swift b/ClientTests/SearchEnginesTests.swift
index 6ce621444..6afdb5020 100644
--- a/ClientTests/SearchEnginesTests.swift
+++ b/ClientTests/SearchEnginesTests.swift
@@ -196,4 +196,58 @@ class SearchEnginesTests: XCTestCase {
XCTAssertEqual(engines.orderedEngines.first!.shortName, "Google", "Google should be the first search engine")
}
+ func testSearchEngineParamsNewUser() {
+ Preferences.General.isFirstLaunch.value = true
+
+ let profile = MockProfile()
+ profile.searchEngines.regionalSearchEngineSetup(for: Locale(identifier: "de-DE"))
+
+ expectddgClientName(locales: ["de-DE"], expectedClientName: "bravened", profile: profile)
+ expectddgClientName(locales: ["en-IE", "en-AU", "en-NZ"], expectedClientName: "braveed", profile: profile)
+ expectddgClientName(locales: ["en-US, pl-PL"],
+ expectedClientName: OpenSearchEngine.defaultSearchClientName, profile: profile)
+
+ XCTAssert(getQwant(profile: profile).searchURLForQuery("test")!.absoluteString.contains("client=brz-brave"))
+ }
+
+ func testSearchEngineParamsExistingUser() {
+ Preferences.General.isFirstLaunch.value = false
+
+ let profile = MockProfile()
+ expectddgClientName(locales: ["de-DE"], expectedClientName: "bravened", profile: profile)
+ expectddgClientName(locales: ["en-IE", "en-AU", "en-NZ"], expectedClientName: "braveed", profile: profile)
+ expectddgClientName(locales: ["en-US, pl-PL"],
+ expectedClientName: OpenSearchEngine.defaultSearchClientName, profile: profile)
+
+ XCTAssert(getQwant(profile: profile).searchURLForQuery("test")!.absoluteString.contains("client=brz-brave"))
+ }
+
+ private func expectddgClientName(locales: [String], expectedClientName: String, profile: Profile) {
+ locales.forEach {
+ let ddg = getDdg(profile: profile)
+ let locale = Locale(identifier: $0)
+ print(ddg.searchURLForQuery("test", locale: locale)!)
+
+ let query = ddg.searchURLForQuery("test", locale: locale)
+ XCTAssertNotNil(query)
+
+ let tParam = URLComponents(url: query!, resolvingAgainstBaseURL: false)?
+ .queryItems?
+ .first(where: { $0.name == "t" })
+
+ XCTAssertEqual(tParam?.value, expectedClientName)
+ }
+ }
+
+ private func getDdg(profile: Profile) -> OpenSearchEngine {
+ profile.searchEngines.orderedEngines.first(where: {
+ $0.shortName == OpenSearchEngine.EngineNames.duckDuckGo
+ })!
+ }
+
+ private func getQwant(profile: Profile) -> OpenSearchEngine {
+ return profile.searchEngines.orderedEngines.first(where: {
+ $0.shortName == OpenSearchEngine.EngineNames.qwant
+ })!
+ }
}