From f5d3e780dd52096f47a8837a133eec03b71377c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=C2=A0Buczek?= Date: Wed, 4 Sep 2019 01:18:32 +0200 Subject: [PATCH 1/2] Custom client for ddg. Ref: internal#648 --- .../Application/Delegates/AppDelegate.swift | 4 +- Client/Assets/SearchPlugins/duckduckgo.xml | 4 +- .../Frontend/Browser/Search/OpenSearch.swift | 68 ++++++++++++------- .../Browser/Search/SearchEngines.swift | 11 ++- ClientTests/SearchEnginesTests.swift | 54 +++++++++++++++ 5 files changed, 111 insertions(+), 30 deletions(-) diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift index 199f4b049..f1ca1ca02 100644 --- a/Client/Application/Delegates/AppDelegate.swift +++ b/Client/Application/Delegates/AppDelegate.swift @@ -237,12 +237,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati Preferences.General.basicOnboardingCompleted.value = isFirstLaunch ? OnboardingState.unseen.rawValue : OnboardingState.completed.rawValue } - Preferences.General.isFirstLaunch.value = false Preferences.Review.launchCount.value += 1 if isFirstLaunch { FavoritesHelper.addDefaultFavorites() - profile?.searchEngines.setupDefaultRegionalSearchEngines() + profile?.searchEngines.regionalSearchEngineSetup() } if let urp = UserReferralProgram.shared { if isFirstLaunch { @@ -259,6 +258,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati } AdblockResourceDownloader.shared.startLoading() + Preferences.General.isFirstLaunch.value = false return shouldPerformAdditionalDelegateHandling } 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 + })! + } } From 2a8120119093d2fc1a239d307585841b2059b7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=C2=A0Buczek?= Date: Mon, 30 Sep 2019 16:14:58 +0200 Subject: [PATCH 2/2] Move `isFirstLaunch` back --- Client/Application/Delegates/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/Application/Delegates/AppDelegate.swift b/Client/Application/Delegates/AppDelegate.swift index f1ca1ca02..b2bc644b5 100644 --- a/Client/Application/Delegates/AppDelegate.swift +++ b/Client/Application/Delegates/AppDelegate.swift @@ -237,6 +237,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati Preferences.General.basicOnboardingCompleted.value = isFirstLaunch ? OnboardingState.unseen.rawValue : OnboardingState.completed.rawValue } + Preferences.General.isFirstLaunch.value = false Preferences.Review.launchCount.value += 1 if isFirstLaunch { @@ -258,7 +259,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati } AdblockResourceDownloader.shared.startLoading() - Preferences.General.isFirstLaunch.value = false return shouldPerformAdditionalDelegateHandling }