mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 07:48:38 +09:00
291 lines
12 KiB
Swift
291 lines
12 KiB
Swift
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import Foundation
|
|
import Shared
|
|
import Storage
|
|
import XCGLogger
|
|
|
|
private let log = Logger.browserLogger
|
|
|
|
private let OrderedEngineNames = "search.orderedEngineNames"
|
|
private let DisabledEngineNames = "search.disabledEngineNames"
|
|
private let ShowSearchSuggestionsOptIn = "search.suggestions.showOptIn"
|
|
private let ShowSearchSuggestions = "search.suggestions.show"
|
|
private let customSearchEnginesFileName = "customEngines.plist"
|
|
|
|
/**
|
|
* Manage a set of Open Search engines.
|
|
*
|
|
* The search engines are ordered. Individual search engines can be enabled and disabled. The
|
|
* first search engine is distinguished and labeled the "default" search engine; it can never be
|
|
* disabled. Search suggestions should always be sourced from the default search engine.
|
|
*
|
|
* Two additional bits of information are maintained: whether the user should be shown "opt-in to
|
|
* search suggestions" UI, and whether search suggestions are enabled.
|
|
*
|
|
* Consumers will almost always use `defaultEngine` if they want a single search engine, and
|
|
* `quickSearchEngines()` if they want a list of enabled quick search engines (possibly empty,
|
|
* since the default engine is never included in the list of enabled quick search engines, and
|
|
* it is possible to disable every non-default quick search engine).
|
|
*
|
|
* The search engines are backed by a write-through cache into a ProfilePrefs instance. This class
|
|
* is not thread-safe -- you should only access it on a single thread (usually, the main thread)!
|
|
*/
|
|
class SearchEngines {
|
|
fileprivate let prefs: Prefs
|
|
fileprivate let fileAccessor: FileAccessor
|
|
|
|
init(prefs: Prefs, files: FileAccessor) {
|
|
self.prefs = prefs
|
|
// By default, show search suggestions
|
|
self.shouldShowSearchSuggestions = prefs.boolForKey(ShowSearchSuggestions) ?? true
|
|
self.fileAccessor = files
|
|
self.disabledEngineNames = getDisabledEngineNames()
|
|
self.orderedEngines = getOrderedEngines()
|
|
}
|
|
|
|
var defaultEngine: OpenSearchEngine {
|
|
get {
|
|
return self.orderedEngines[0]
|
|
}
|
|
|
|
set(defaultEngine) {
|
|
// The default engine is always enabled.
|
|
self.enableEngine(defaultEngine)
|
|
// The default engine is always first in the list.
|
|
var orderedEngines = self.orderedEngines.filter { engine in engine.shortName != defaultEngine.shortName }
|
|
orderedEngines.insert(defaultEngine, at: 0)
|
|
self.orderedEngines = orderedEngines
|
|
}
|
|
}
|
|
|
|
func isEngineDefault(_ engine: OpenSearchEngine) -> Bool {
|
|
return defaultEngine.shortName == engine.shortName
|
|
}
|
|
|
|
// The keys of this dictionary are used as a set.
|
|
fileprivate var disabledEngineNames: [String: Bool]! {
|
|
didSet {
|
|
self.prefs.setObject(Array(self.disabledEngineNames.keys), forKey: DisabledEngineNames)
|
|
}
|
|
}
|
|
|
|
var orderedEngines: [OpenSearchEngine]! {
|
|
didSet {
|
|
self.prefs.setObject(self.orderedEngines.map { $0.shortName }, forKey: OrderedEngineNames)
|
|
}
|
|
}
|
|
|
|
var quickSearchEngines: [OpenSearchEngine]! {
|
|
get {
|
|
return self.orderedEngines.filter({ (engine) in !self.isEngineDefault(engine) && self.isEngineEnabled(engine) })
|
|
}
|
|
}
|
|
|
|
var shouldShowSearchSuggestions: Bool {
|
|
didSet {
|
|
self.prefs.setObject(shouldShowSearchSuggestions, forKey: ShowSearchSuggestions)
|
|
}
|
|
}
|
|
|
|
func isEngineEnabled(_ engine: OpenSearchEngine) -> Bool {
|
|
return disabledEngineNames.index(forKey: engine.shortName) == nil
|
|
}
|
|
|
|
func enableEngine(_ engine: OpenSearchEngine) {
|
|
disabledEngineNames.removeValue(forKey: engine.shortName)
|
|
}
|
|
|
|
func disableEngine(_ engine: OpenSearchEngine) {
|
|
if isEngineDefault(engine) {
|
|
// Can't disable default engine.
|
|
return
|
|
}
|
|
disabledEngineNames[engine.shortName] = true
|
|
}
|
|
|
|
func deleteCustomEngine(_ engine: OpenSearchEngine) {
|
|
// We can't delete a preinstalled engine or an engine that is currently the default.
|
|
if !engine.isCustomEngine || isEngineDefault(engine) {
|
|
return
|
|
}
|
|
|
|
customEngines.remove(at: customEngines.index(of: engine)!)
|
|
saveCustomEngines()
|
|
orderedEngines = getOrderedEngines()
|
|
}
|
|
|
|
/// Adds an engine to the front of the search engines list.
|
|
func addSearchEngine(_ engine: OpenSearchEngine) {
|
|
customEngines.append(engine)
|
|
orderedEngines.insert(engine, at: 1)
|
|
saveCustomEngines()
|
|
}
|
|
|
|
func queryForSearchURL(_ url: URL?) -> String? {
|
|
for engine in orderedEngines {
|
|
guard let searchTerm = engine.queryForSearchURL(url) else { continue }
|
|
return searchTerm
|
|
}
|
|
return nil
|
|
}
|
|
|
|
fileprivate func getDisabledEngineNames() -> [String: Bool] {
|
|
if let disabledEngineNames = self.prefs.stringArrayForKey(DisabledEngineNames) {
|
|
var disabledEngineDict = [String: Bool]()
|
|
for engineName in disabledEngineNames {
|
|
disabledEngineDict[engineName] = true
|
|
}
|
|
return disabledEngineDict
|
|
} else {
|
|
return [String: Bool]()
|
|
}
|
|
}
|
|
|
|
fileprivate func customEngineFilePath() -> String {
|
|
let profilePath = try! self.fileAccessor.getAndEnsureDirectory() as NSString
|
|
return profilePath.appendingPathComponent(customSearchEnginesFileName)
|
|
}
|
|
|
|
fileprivate lazy var customEngines: [OpenSearchEngine] = {
|
|
return NSKeyedUnarchiver.unarchiveObject(withFile: self.customEngineFilePath()) as? [OpenSearchEngine] ?? []
|
|
}()
|
|
|
|
fileprivate func saveCustomEngines() {
|
|
NSKeyedArchiver.archiveRootObject(customEngines, toFile: self.customEngineFilePath())
|
|
}
|
|
|
|
/// Return all possible paths for a language identifier in the order of most specific to least specific.
|
|
/// For example, zh-Hans-CN with a default of en will return [zh-Hans-CN, zh-CN, zh, en]. The fallback
|
|
/// identifier must be a known one that is guaranteed to exist in the SearchPlugins directory.
|
|
class func directoriesForLanguageIdentifier(_ languageIdentifier: String, basePath: NSString, fallbackIdentifier: String) -> [String] {
|
|
var directories = [String]()
|
|
let components = languageIdentifier.components(separatedBy: "-")
|
|
if components.count == 1 {
|
|
// zh
|
|
directories.append(languageIdentifier)
|
|
} else if components.count == 2 {
|
|
// zh-CN
|
|
directories.append(languageIdentifier)
|
|
directories.append(components[0])
|
|
} else if components.count == 3 {
|
|
directories.append(languageIdentifier)
|
|
directories.append(components[0] + "-" + components[2])
|
|
directories.append(components[0])
|
|
}
|
|
if !directories.contains(fallbackIdentifier) {
|
|
directories.append(fallbackIdentifier)
|
|
}
|
|
|
|
return directories.map { (path) -> String in
|
|
return basePath.appendingPathComponent(path)
|
|
}
|
|
}
|
|
|
|
// Return the language identifier to be used for the search engine selection. This returns the first
|
|
// identifier from preferredLanguages and takes into account that on iOS 8, zh-Hans-CN is returned as
|
|
// zh-Hans. In that case it returns the longer form zh-Hans-CN. Same for traditional Chinese.
|
|
//
|
|
// These exceptions can go away when we drop iOS 8 or when we start using a better mechanism for search
|
|
// engine selection that is not based on language identifier.
|
|
class func languageIdentifierForSearchEngines() -> String {
|
|
let languageIdentifier = Locale.preferredLanguages.first!
|
|
switch languageIdentifier {
|
|
case "zh-Hans":
|
|
return "zh-Hans-CN"
|
|
case "zh-Hant":
|
|
return "zh-Hant-TW"
|
|
default:
|
|
return languageIdentifier
|
|
}
|
|
}
|
|
|
|
/// Get all bundled (not custom) search engines, with the default search engine first,
|
|
/// but the others in no particular order.
|
|
class func getUnorderedBundledEngines() -> [OpenSearchEngine] {
|
|
let pluginBasePath: NSString = (Bundle.main.resourcePath! as NSString).appendingPathComponent("SearchPlugins") as NSString
|
|
let languageIdentifier = languageIdentifierForSearchEngines()
|
|
let fallbackDirectory: NSString = pluginBasePath.appendingPathComponent("en") as NSString
|
|
|
|
var directory: String?
|
|
for path in directoriesForLanguageIdentifier(languageIdentifier, basePath: pluginBasePath, fallbackIdentifier: "en") {
|
|
if FileManager.default.fileExists(atPath: path) {
|
|
directory = path
|
|
break
|
|
}
|
|
}
|
|
|
|
// This cannot happen if we include the fallback, but if it does we return no engines at all
|
|
guard let searchDirectory = directory else {
|
|
return []
|
|
}
|
|
|
|
let index = (searchDirectory as NSString).appendingPathComponent("list.txt")
|
|
let listFile = try? String(contentsOfFile: index, encoding: String.Encoding.utf8)
|
|
assert(listFile != nil, "Read the list of search engines")
|
|
|
|
let engineNames = listFile!
|
|
.trimmingCharacters(in: CharacterSet.newlines)
|
|
.components(separatedBy: CharacterSet.newlines)
|
|
|
|
var engines = [OpenSearchEngine]()
|
|
let parser = OpenSearchParser(pluginMode: true)
|
|
for engineName in engineNames {
|
|
// Ignore hidden engines in list.txt
|
|
if engineName.endsWith(":hidden") {
|
|
continue
|
|
}
|
|
|
|
// Search the current localized search plugins directory for the search engine.
|
|
// If it doesn't exist, fall back to English.
|
|
var fullPath = (searchDirectory as NSString).appendingPathComponent("\(engineName).xml")
|
|
if !FileManager.default.fileExists(atPath: fullPath) {
|
|
fullPath = fallbackDirectory.appendingPathComponent("\(engineName).xml")
|
|
}
|
|
assert(FileManager.default.fileExists(atPath: fullPath), "\(fullPath) exists")
|
|
|
|
guard let engine = parser.parse(fullPath, engineID: engineName) else {
|
|
log.error("Failed to parse search engine ID \(engineName) at \(fullPath)")
|
|
continue
|
|
}
|
|
engines.append(engine)
|
|
}
|
|
|
|
let defaultEngineFile = (searchDirectory as NSString).appendingPathComponent("default.txt")
|
|
let defaultEngineName = try? String(contentsOfFile: defaultEngineFile, encoding: String.Encoding.utf8).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
|
|
return engines.sorted { e, _ in e.shortName == defaultEngineName }
|
|
}
|
|
|
|
/// Get all known search engines, possibly as ordered by the user.
|
|
fileprivate func getOrderedEngines() -> [OpenSearchEngine] {
|
|
let unorderedEngines = customEngines + SearchEngines.getUnorderedBundledEngines()
|
|
|
|
guard let orderedEngineNames = prefs.stringArrayForKey(OrderedEngineNames) else {
|
|
// We haven't persisted the engine order, so return whatever order we got from disk.
|
|
return unorderedEngines
|
|
}
|
|
|
|
// We have a persisted order of engines, so try to use that order.
|
|
// We may have found engines that weren't persisted in the ordered list
|
|
// (if the user changed locales or added a new engine); these engines
|
|
// will be appended to the end of the list.
|
|
return unorderedEngines.sorted { engine1, engine2 in
|
|
let index1 = orderedEngineNames.index(of: engine1.shortName)
|
|
let index2 = orderedEngineNames.index(of: engine2.shortName)
|
|
|
|
if index1 == nil && index2 == nil {
|
|
return engine1.shortName < engine2.shortName
|
|
}
|
|
|
|
// nil < N for all non-nil values of N.
|
|
if index1 == nil || index2 == nil {
|
|
return index1 ?? -1 > index2 ?? -1
|
|
}
|
|
|
|
return index1! < index2!
|
|
}
|
|
}
|
|
}
|