mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-22 00:17:32 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
291
mobile/ios/Client/Frontend/Browser/SearchEngines.swift
Normal file
291
mobile/ios/Client/Frontend/Browser/SearchEngines.swift
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
/* 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!
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue